Security notes for IT

Reviewed against the shipping build on 2026-08-26 · Written for the person who has to approve this app, not for the person who wants to install it

SpectiX is a menu bar app that shows the live state of every Claude Code session on a developer's Mac: which one is working, which one finished, which one is sitting on a permission prompt. It is one signed and notarized bundle, installed per user, with no daemon, no admin rights, and no network stack linked into it.

This page is the evidence for that sentence. Every claim below is followed by a command you can run yourself against the shipped binary, and by a note on what that command does not prove. The second half is deliberate: an evaluator who finds an unstated limitation on their own has learned something worse than the limitation itself.

The short version, if you are approving this for a fleet: it reads local files under ~/.claude/, it writes small status files under ~/.claude/spectix/, it adds one shell script to Claude Code's hook configuration, and the binary opens no outbound connection. Two things can still put traffic on the wire, and neither of them is the app: an optional Apple Watch push sent by that shell script, and the Claude CLI the developer already runs, which the app can start in the background to read a subscription-usage figure. The first is the only outbound path SpectiX adds, and it stays off until somebody configures it; the second is traffic you already permit. Both are named and bounded below.

Jump to: No network · Permissions · Processes it starts · What it writes · The hook · Outbound paths · No telemetry · Limits · Deploy & remove

1. The binary makes no network calls

This is not a privacy-policy sentence. It is clause 4 of the licence accepted at install time, which states that SpectiX performs no network activity: no outbound connection, no account system, no telemetry, no analytics, no crash reporting, no update ping, no licence-check call. Clause 2 then grants you, in writing, the right to verify that from outside the binary — reading its code signature and entitlements, listing its linked libraries, observing its sockets, and running it behind an outbound firewall are all expressly permitted.

Inside the project the guarantee is held up by two build-time gates, so that it cannot quietly rot the first time somebody adds a convenient "check for updates":

Verify the linked libraries yourself:

otool -L /Applications/SpectiX.app/Contents/MacOS/SpectiX Cocoa, AppKit, Foundation, Carbon, CoreGraphics, Swift runtime no CFNetwork · no Network.framework · no libcurl · no libssl

Then watch its sockets. Not "a few harmless connections" — none, on launch and after a week of uptime:

lsof -i -a -p $(pgrep -x SpectiX) no output, at any point in the app's life

Then read what it asked the system for. The entitlement list is covered by the code signature, so it cannot be edited without breaking the signature:

codesign -d --entitlements - /Applications/SpectiX.app one entitlement: com.apple.security.automation.apple-events no com.apple.security.network.client, no network.server

And confirm the bundle is what we shipped, unmodified, and notarized by Apple:

codesign --verify --deep --strict --verbose=2 /Applications/SpectiX.app spctl -a -vvv /Applications/SpectiX.app valid on disk · satisfies its Designated Requirement source=Notarized Developer ID

If you would rather not take any of that on trust, block it outright: add SpectiX to Little Snitch, LuLu, or your MDM's outbound rules, or run the Mac under an egress deny-all. Nothing in the app degrades, because nothing in the app was reaching out.

What these checks do not prove. SpectiX does not enable the App Sandbox, so the absent network entitlement is not the kernel holding it back — a non-sandboxed process with no network entitlement can still open a socket. We cannot enable the sandbox: the app has to read arbitrary paths under ~/.claude/ and drive other applications' windows through the Accessibility API, and the sandbox permits neither. So read the entitlement check as "here is the complete, tamper-evident list of what it asked the system for", not as "the kernel is stopping it". The three checks that actually close that gap are otool -L (it cannot link what it does not link), lsof (it cannot hide a socket from the kernel's own table), and your firewall.

2. It asks for one system permission

SpectiX requests Accessibility. That is the whole list. macOS will additionally show an Automation entry for one specific terminal app the first time a developer uses the "jump to my terminal" action, because driving Terminal or iTerm to raise a window is an Apple Event.

Accessibility is what turns a click in the menu bar into the right window coming forward. Four operations use it, and no others: raising a window, locating a split pane so the highlight ring lands on the correct one, reading window titles so a session's working directory can be matched to the editor window that owns it, and detecting the state of the Claude desktop app.

What it never asks for:

Permission Status
Screen Recording Not requested, and never prompted for. An early version read cross-Space window titles through CGWindowListCopyWindowInfo, which macOS gates behind Screen Recording. That was removed — titles now come from the Accessibility API. Nothing in the app captures pixels.
Full Disk Access Not requested. It reads ~/.claude/, which is the user's own home directory and needs no TCC grant.
Files & Folders Not requested — no Documents, Desktop, or Downloads prompt.
Camera · Microphone Not requested.
Contacts · Calendar · Reminders · Photos Not requested.
Location Not requested.
Admin · sudo · root Never asked, at install time or afterwards. See Deploy & remove.

Walk the list yourself in System Settings › Privacy & Security. SpectiX appears under Accessibility, and under Automation for whichever terminal app it has been asked to raise. It appears nowhere else.

What this section does not claim. Accessibility is a genuinely powerful grant: any app holding it can observe and drive other applications' windows, and we are not going to tell you it is harmless. What we can tell you is what it is used for — the four operations above — that it is revocable at any time in System Settings, and that revoking it costs the click-to-jump feature and nothing else. The status rows keep working without it.

3. Processes it starts, and what your EDR will see

An app that raises other applications' windows has to talk to them, and on macOS that means starting short-lived child processes. Endpoint detection tools ship rules for some of these, so here is the complete list rather than a surprise in someone's alert queue.

Process When, and how often
/usr/bin/osascript Only when the developer clicks a row that lives in Terminal or iTerm. The script raises that window and selects the right tab — nothing else. This is the process most likely to be flagged by an EDR rule, which is exactly why it is named here. It runs as SpectiX rather than disclaimed, so the resulting Automation prompt is attributed to SpectiX and can be pre-approved by profile.
/usr/bin/open Only when the developer clicks a row that lives in VS Code, Cursor, or Windsurf: the editor bundle is asked to bring the right window forward.
claude (the CLI already installed) Background probe for the subscription-usage figure shown in the header, rate limited to once per minute at most. This is the developer's own Claude Code CLI, and it talks to Anthropic exactly as it does when they run it by hand — see Limits. It is started disclaimed, so any permission prompt the CLI triggers is attributed to the CLI and not to SpectiX.
/usr/bin/tccutil Only when someone presses "reset permission" in Settings, to clear this app's own Accessibility grant so macOS will prompt again.

One process is deliberately not in that table. /usr/bin/curl appears on the machine when the optional Apple Watch push is configured, but it is started by the hook script described in section 5 — a separate process in the user's own home directory, not a child of the app. That distinction is the whole reason the no-network guarantee survives the feature, and it is examined in section 6.

4. Everything it stores stays in one visible folder

All state lives in ~/.claude/spectix/. It is a plain directory of small plain files — nothing there is encoded, compressed, or obfuscated. Open it and read it:

ls -la ~/.claude/spectix/ tail -3 ~/.claude/spectix/events.jsonl

Files ending in -<tty> exist once per terminal, keyed to the terminal device name such as ttys003. Sessions with no terminal — the editor's chat panel — key on the process id instead. The Derived from work column is the one to read closely: it marks every file whose contents come from what the developer is doing, rather than from counters and timestamps.

File Contents Derived from work
state-<tty> One word: working, done, needs, paused, idle. no
title-<tty> The row label for that session — a short summary of the current task, or the first few words of the prompt. yes
step-<tty> The tool step in flight, such as the name of the file being read. yes
tp-<tty> The absolute path of that session's transcript file. A path, not its contents — but the path contains the project directory name. yes
agents-<tty>
agent-step-<tty>-<id>
Which subagents are still running and what each is doing. The roster carries each subagent's task description. yes
title-ai-used-<tty>
title-ai-stamp-<tty>
Bookkeeping so a repeated row label is not applied twice. title-ai-used holds the label itself; title-ai-stamp is only a timestamp. yes
bg-<tty> Which background shell commands are still running, by identifier. no
ctx-<tty> How full that session's context window is, as a number. no
tick-<tty> A heartbeat timestamp, written at most once a minute. no
clear-boundary-<tty> A timestamp marking the last context reset, used as the baseline for token counting. no
events.jsonl Append-only, one line per counted event, feeding the local statistics window. Each line holds a timestamp, the local date, the event kind, the basename of the session's working directory, the terminal name, and on completed turns the token tallies, round-trip count and model id. Lines for a new turn also carry a prompt summary, truncated to 80 characters — the same short string shown on the row. yes
projects.json Recently seen project paths, so the "recent projects" list survives a restart. These are absolute paths. yes
jump-diag.log A capped diagnostic log of how each jump was resolved. The home directory is replaced with ~, but project and file names are kept on purpose so a misrouted jump can be explained. It self-truncates and is written by release builds too, not only by development ones. yes
terminals-<pid>.json Written by the editor extension rather than by the app: the list of open terminal tabs in one editor window, with their titles. Claude Code renames those tabs after the task in flight, so the titles carry task descriptions. One file per editor window. yes
impact.jsonl Append-only log behind the effectiveness panel: when a jump was offered, whether it was taken, how long the developer stayed. Counters and timestamps only — no project names, no titles. no
usage.json A snapshot of Claude subscription usage percentages and the next reset time. See Limits. no
popover-debug.log
ring-diag.log
focusring-sweep.log
Capped diagnostic logs for the menu bar panel and the highlight ring: screen coordinates, window identifiers and timings. No titles, no paths. Written by release builds too. no
desktop-windows.json
desktop-design-wids.json
desktop-probe.log
Bookkeeping for detecting the Claude desktop app's windows — window identifiers and probe error counts. no
design-ax-dump.request Not created by the app — it is a diagnostic flag you create by hand. While the file exists, the app dumps the accessibility tree of the Claude desktop app's windows into design-tree-*.txt beside it, and an accessibility tree contains the text on screen. Nothing is written unless you put the flag there; delete it to stop. Listed here because it is the one thing in this folder that can capture conversation text, even though it takes a deliberate act to switch on. only if you turn it on
active-terminal
active-window
focus-request
chat-focus-request
open-terminal-request
One-shot request files used by the app and the editor extension to talk to each other. Written and consumed immediately; one of them carries the path of the directory to open. partly
icons/ The per-project icons the user picked. no
sound-done
sound-needs
sound-volume
Sound preferences. no
push-key
push-server
push-done
push-last
push-at-<tty>-<state>
Settings and rate-limit timestamps for the optional Apple Watch push, plus the outcome of the last attempt. Absent entirely until the feature is configured. no

Read the marked rows plainly. Nine of them carry material derived from the developer's work: a short row label, the current tool step, a transcript path, subagent task descriptions, the bookkeeping file that holds a repeated label, recently opened project paths, project names with an 80-character prompt summary in the statistics log, the jump diagnostic log that keeps project and file names on purpose, and editor tab titles. None of those nine contains source code, a full prompt, or model output, and none of them is transmitted anywhere. The single thing in this folder that can capture text off the screen is the diagnostic flag two rows above, and it produces nothing until somebody creates it by hand. That is the honest shape of it: the claim is "it stays here", not "there is nothing to see".

There is no in-app switch that stops the statistics log. If you do not want it, delete the file, or remove the hook wiring in section 5 — which is what writes it. The app treats a missing log as an empty one and carries on. Application preferences live in the standard per-user location rather than in that folder:

defaults read app.spectix.SpectiX

What this section does not claim. The app also reads files it does not own: the last 256 KB of the current Claude Code transcript, to learn which model the session is on and how full its context window is, and the session directory as a fallback status signal. It reads them; it does not copy them and it does not send them. But it is a read of a conversation, and you should know that before approving it.

5. It installs one hook script, and a helper beside it

SpectiX does not read terminal buffers, attach a debugger, or scrape the screen. It learns a session's state because Claude Code tells it: a hook script runs on the events that matter and writes one word to a file. Two files are installed, both plain text — the hook itself, and a small parser it calls for the subscription-usage figure:

~/.claude/hooks/spectix-status.sh ~/.claude/hooks/spectix-usage.py

and seven entries are merged into the hooks block of ~/.claude/settings.json:

UserPromptSubmit → spectix-status.sh working PreToolUse → spectix-status.sh working PostToolUse → spectix-status.sh working Stop → spectix-status.sh done Notification → spectix-status.sh needs PermissionRequest → spectix-status.sh needs SessionStart → spectix-status.sh session-start

What the script does, in order: work out which terminal fired the event by walking up the process chain; write that session's state, label, step and counters into ~/.claude/spectix/, each through a temporary file and a rename so a reader never sees a half-written file; optionally play a system sound; optionally send the Apple Watch push described in section 6; and append one line to the local statistics log. It returns in milliseconds and makes no network call other than that opt-in push.

Two details matter if you are diffing configuration files across a fleet, and they are easy to mistake for one behaviour:

Read the script before trusting it — it is commented throughout, and it is the same copy that ships inside the bundle, so a diff proves nothing rewrote it after install:

less ~/.claude/hooks/spectix-status.sh diff ~/.claude/hooks/spectix-status.sh \ /Applications/SpectiX.app/Contents/Resources/hooks/spectix-status.sh

6. The one outbound path SpectiX adds, and why it is not a contradiction

SpectiX can buzz an Apple Watch when a session stops and waits for input. That notification does leave the machine — and it is still true that the binary makes no network calls, because the binary is not what sends it. The push is made by the hook script, a separate process, using curl. Run the same otool -L and lsof checks from section 1 with the feature switched on and the result is unchanged.

The detour exists because a local macOS notification cannot reach a Watch at all: watchOS mirrors notifications that the paired iPhone received, and a Mac is not in that chain. Reaching the wrist means going out to a push service. How the feature is bounded:

If your policy is no egress at all. Leave the key empty and there is nothing to block. If you want to be categorical, delete the push function from ~/.claude/hooks/spectix-status.sh — the rest of the script does not depend on it. And it remains blockable by ordinary means: it is curl running from a script in the user's own home directory, subject to the same firewall rules as everything else there.

7. No telemetry, no account, no crash reporting

Each of those is a term of clause 4 of the licence, not a policy that can be quietly revised. If a future version ever introduces network activity, the licence binds us to make it opt-in, to document it before it ships, and to announce it in the changelog. It could not be switched on by a background update in any case, because there is no background update.

One thing to read correctly: SpectiX does show usage statistics — turns per day, per project, token tallies, estimated cost. Those are computed on the developer's own Mac from the local log in section 4 and displayed to them. They are theirs. We never see them.

8. Limits, in our own words

Five things an evaluator should know, which we would rather state than have you discover.

9. Deploying it, and removing it completely

Requirements. macOS 13.0 or later, Apple Silicon or Intel — one universal binary. No admin rights, no sudo, and no installer package that runs as root.

What an install touches. The application bundle goes to /Applications, or to ~/Applications if that account cannot write to the system folder — the installer falls back rather than asking for a password. Beyond that: the two hook scripts and the wiring from section 5, the state directory from section 4, optionally an editor extension in ~/.vscode/extensions/ and the equivalent folder for Cursor and Windsurf, and one per-user login item registered on first launch through the system's login-item service. A monitor that is not running tells you nothing, so it starts enabled; switching it off in System Settings or in the app keeps it off, and it is never re-registered.

There is no LaunchDaemon, no LaunchAgent property list of our own, no kernel extension, no background service that survives quitting the app, and no file outside the account that installed it.

Fleet deployment

The app is a self-contained bundle, so copying it into /Applications through your MDM is a complete install. On first launch it copies its hook scripts into the user's hooks directory and merges the wiring, both idempotently, so re-deploying over an existing install changes nothing.

The Accessibility grant is per user and per binary. If you would rather your users never saw the prompt, pre-approve it with a PPPC payload — a ready-made profile is below, and it is deliberately published for anyone to use, MDM-managed fleets included.

Signing identity to allow-list:

Bundle identifier app.spectix.SpectiX Team identifier 5CQ7ZRVZ8R Code requirement identifier "app.spectix.SpectiX" and anchor apple generic and certificate leaf[subject.OU] = "5CQ7ZRVZ8R"

Verify those against the copy you downloaded before you trust this page for them:

codesign -dv --verbose=2 /Applications/SpectiX.app Identifier=app.spectix.SpectiX TeamIdentifier=5CQ7ZRVZ8R

The profile grants Accessibility, plus Apple Events to Terminal and iTerm for the jump-to-terminal action. Import it into Jamf, Kandji, Intune or any other MDM as a PPPC payload; it declares no other service, and the app requests none.

Download the PPPC profile

Complete removal

Quit SpectiX from the menu bar icon. Then, while the bundle is still on disk, revoke the grant under System Settings › Privacy & Security › Accessibility and remove the entry under System Settings › General › Login Items.

That order is not cosmetic. Once the bundle is deleted, macOS can no longer resolve the identifier, the revocation fails silently, and an orphaned permission record is left behind. The same trap applies to preferences: run defaults delete rather than deleting a preference file by hand, because the preferences daemon holds those values in memory and writes them straight back to the file you just removed.

Only then remove the files:

defaults delete app.spectix.SpectiX killall cfprefsd rm -rf ~/.claude/spectix rm -f ~/.claude/hooks/spectix-status.sh ~/.claude/hooks/spectix-usage.py rm -rf ~/.vscode/extensions/spectix.focus-* rm -rf /Applications/SpectiX.app

Finally, remove the entries whose command contains spectix-status.sh from the hooks block of ~/.claude/settings.json — your own hooks are untouched by this — and repeat the extension line for Cursor and Windsurf if either is installed.

That is the whole footprint. Nothing in /Library, nothing in /usr/local, and no receipt in the package database, because nothing was installed as root.

Questions this page did not answer

The licence is the only binding text; this page is a reading of it. For the individual-user version of the same story — written for the developer rather than for the reviewer — see Privacy & permissions. For what changed in each build, including anything that would ever alter clause 4, see the changelog. Anything still unanswered: write to us — mail reaches a person.

Download SpectiX