Why AgentBar reads files instead of spawning your agents
CodexBar froze a Mac every minute because it spawned Claude CLI to read usage. AgentBar reads the files agents already write, and tests prove it spawns nothing.
By tsuvic · Published Aug 1, 2026 · 6 min read
Last July my Mac started freezing for nearly a minute, once a minute. The pointer moved, stopped, then caught up in a rush; keystrokes queued and arrived late. I assumed a runaway Chrome tab had finally won. It hadn't. The process tree pointed at a menu bar app I had installed to keep an eye on my coding agents, and the thing it kept starting, sixty seconds apart, was the agent itself.
The app was CodexBar 0.43.0, Apple-notarized, with its refresh interval set to one minute. To show me a usage number, it spawned the Claude CLI. The CLI did not run in a clean sandbox. It read my ordinary Claude configuration, which meant every probe also initialized my two MCP servers (Context7 and Serena) and my Chrome integration. Chrome arrived with its GPU process, network service, storage service, and a swarm of renderers restoring tabs and extensions.
The measurements from that afternoon are in the repository, with timestamps. Two probes started at 14:56:48 and 14:57:48, one minute apart, both children of CodexBar's watchdog. Renderers individually took 30 to 111 percent of a core. Free memory fell from roughly 220,000 pages to about 4,000; compressed memory went from zero to over 100,000 pages; WindowServer climbed to 33–40 percent CPU, and input stopped reaching the screen. I killed CodexBar at 14:58:42 and watched for two more minutes. No new probes appeared.
To be fair to CodexBar, it was not the only load on that machine. Two USB display tools (DisplayLink and InstantView) were also running, and they amplify anything that stresses WindowServer. But the periodic trigger, the thing that turned background weight into a frozen desktop every sixty seconds, was a menu bar app reading usage the expensive way.
Reading usage should not cost a CLI
I went through CodexBar's GitHub issues afterwards, about two hundred of them, and the same design shows up everywhere. When your data source is "ask the CLI," and the CLI is a full agent harness, every refresh carries the weight of your whole setup. The more MCP servers and integrations you configure, the heavier the menu bar update becomes.
The issue tracker makes the pattern concrete:
- #1844: an hourly "delegated CLI refresh" recovery path called
/usr/bin/openand launched the user's default browser, in a fast retry loop every 20 seconds to 3 minutes. - #2052: background Claude probes triggered CLI auto-update downloads because
DISABLE_AUTOUPDATER=1was never set. Ninety gigabytes of traffic in three days, and 257 to 495 Claude CLI processes spawned per day. - #2251: every usage probe minted a new empty Claude session. Thirty-six phantom sessions accumulated in the account within weeks, and the probes started tripping Anthropic's rate limits.
- #2241: the CLI probe succeeded 23 times out of 268, an 8.6 percent hit rate. On failure it threw away the cached snapshot, leaving gaps of up to 110 minutes in the displayed data.
- #1999: two CodexBarCLI processes grew to 75 GB combined on a 48 GB machine and took the whole system down with a jetsam OOM.
None of these are carelessness. They are what happens when a passive monitor keeps booting an active agent, one probe at a time.
The files were already there
The alternative was sitting on disk the whole time. Coding agents record their own work. Claude Code writes ~/.claude/projects/**/*.jsonl. Codex writes ~/.codex/sessions/YYYY/MM/DD/*.jsonl. Qwen writes chat JSONL plus a usage_record.jsonl. Gemini writes event-sourced session JSONL. Cursor keeps its state in a SQLite database (state.vscdb), which AgentBar reads directly through the sqlite3 C API instead of asking Cursor for anything.
Everything AgentBar shows comes from those files. Daily token usage, quotas, per-model breakdowns, sixty days of session history, the graph of which agent called which, even the "waiting for your approval" state. That last one is not a heuristic. For Claude, it is read from the tail of the raw JSONL: an unfinished tool_use record with no matching result means the agent is blocked on a human, and AgentBar pins those rows to the top.
Claude's quota numbers come from ~/.claude/usage-watch/rate-limits.json, a file the Claude CLI itself writes after each exchange. AgentBar never asks a server and never starts a process to get them.
Enforced, not promised
"Read-only" is easy to claim and hard to keep, so AgentBar does not rely on discipline. Two mechanisms make it structural.
At runtime, ProcessGuard diffs the system process table with sysctl around every fetch. If a refresh ever spawns a forbidden child (Node, Python, npm, uv, a browser, an MCP server), the monitor notices its own transgression and reports it. In the test suite, NoProcessSpawnTests goes further: it registers ten MCP servers into a Claude configuration, runs a hundred refresh cycles against it, and fails the build if a single Node, Python, npm, uv, Chrome, or MCP process appears. The test is the receipt.
The rest of the design follows from the incident list above. A self-monitor measures AgentBar's own CPU and RSS with getrusage and task_info, and stops automatic refreshes if it ever crosses a threshold. Failed fetches do not blank the UI; the last good value stays on screen with its timestamp, stale-while-revalidate, which is the direct opposite of issue #2241. The shortest refresh interval on offer is five minutes. There is no one-minute option, by design. Refreshes are single-flight per provider, staggered seconds apart, and a provider that keeps failing gets its circuit breaker opened for fifteen minutes.
The only place AgentBar spawns subprocesses at all is the tool updater, on demand, with hard timeouts, and only when you click it. The Diag tab shows the books: last fetch duration, CPU time, peak RSS, files read, records parsed, and forbidden processes spawned. That last counter is always zero.
What you give up
Reading files has a cost, and it is honesty about that cost that separates a monitor from an ad.
File reading sees what the agent has already flushed to disk, not live API truth. Quota numbers are exactly as fresh as the file that carries them. Run-state detection is precise for Claude, whose log tails make the state explicit, and best-effort for Codex, Qwen, Gemini, and Cursor, where it is inferred from conversation turns. Cost figures are estimates against a model price table, and they may well be covered by a subscription; the UI says "estimated" every time. Cached tokens are priced separately from generated ones so estimates do not double-bill, which is the bug behind CodexBar's issue #1796, where costs ran 2.7 to 4.6 times too high.
And an agent that keeps no local records cannot be watched this way, full stop. For the rare case where a provider's data only exists remotely, AgentBar has a remote usage query, but it is opt-in, configured per provider, and the single exception to the read-only rule. Nothing leaves the machine by default.
The quiet part
The fix that afternoon was quitting an app. The reason to write a new one was the realization that "monitor" and "spawn" had no business being the same verb. A usage meter should get heavier when your agents work harder, never when you configure more tools.
My Mac has been quiet since, which is an observation about my Mac, not a promise about yours. The promise lives in the repository: in ProcessGuard, in NoProcessSpawnTests, and in a Diag tab that prints the receipt on demand.