Summary
Claude is the only provider whose live sources are both budget-gated, which makes its card structurally stale rather than occasionally wrong. Claude Code ≥ 2.1.80 exposes a server-authoritative rate_limits object that costs nothing to read, but the only channel it comes through is the statusLine command — and AGENTS.md:47 says never to rely on the status line for usage parsing.
I think there's a real distinction between what that rule forbids and what this would do, but it's your rule and your call. I'd rather get a ruling before writing code than send you a surprise PR that contradicts a documented line. Happy to implement whichever way you want it, or drop it.
The problem is staleness, not blanking
#2247 (ff52c6d) fixed the blanking half of #2241 — transient parse failures now preserve the last-good snapshot and its timestamp. That part works.
What's left is freshness, and it's structural rather than a parser bug. Both live sources carry the same 5-minute cooldown:
ClaudeOAuthUsageRateLimitGate.swift:6 — defaultCooldown: TimeInterval = 60 * 5, keyed per access token, armed whenever /api/oauth/usage 429s
ClaudeCLIRateLimitGate.swift:5 — same cooldown for the PTY path
Both gates are correct given what they're gating. api.anthropic.com/api/oauth/usage has a tight budget and 429s under polling; the PTY path pays a subprocess plus session-artifact cleanup per probe. The issue history is what that looks like from the outside: #1679 (rate limited), #2117 (slow refresh), #2251 (session-list pollution, since mitigated by the ClaudeProbe artifact cleanup), and the 8.6% probe success rate measured in #2241.
CodexBar has no Claude source that is cheap enough to read often. Every other fix is scheduling against a fixed budget.
The one free channel
Claude Code ≥ 2.1.80 passes a JSON object on stdin to whatever statusLine command is configured. It contains:
{
"rate_limits": {
"five_hour": { "used_percentage": 42.7, "resets_at": 1786099200 },
"seven_day": { "used_percentage": 63.1, "resets_at": 1786608000 }
}
}
This is piggybacked on Messages API responses the session already made, so reading it spends nothing against the usage endpoint's budget. It appears only for Claude.ai-subscriber sessions, and only after the session's first API response.
I checked whether it lands anywhere on disk first, because a file read would need none of this machinery. It doesn't:
- no window fields in
~/.claude/projects/*/*.jsonl session transcripts
- nothing in
~/.claude.json beyond a GrowthBook promo string that happens to contain seven_day
stats-cache.json doesn't carry it either
Corroborating evidence that the hook surface doesn't expose it: orca (MIT) installs the full Claude hook suite — SessionStart, UserPromptSubmit, Stop, PreToolUse — and still had to build a separate statusline route to get rate_limits. If any hook payload carried it, they wouldn't have.
So the statusline slot appears to be the only channel, which is inconvenient given the rule.
The AGENTS.md:47 question
AGENTS.md:47 — "Claude CLI status line is custom + user-configurable; never rely on it for usage parsing."
The argument that this proposal doesn't violate it:
- It reads input, not output. The rule's stated rationale is that the status line is custom and user-configurable — which is true of what a status line renders. This would read the JSON Claude Code supplies to the command. That payload is Anthropic's schema, identical regardless of what the user's status line prints. The rendered text is never inspected.
- The contrast is already in your codebase.
ClaudeStatusProbe scrapes rendered terminal output today — docs/claude.md:195-197: "Strips ANSI, locates 'Current session' + 'Current week' headers." That is the fragile pattern the rule describes. This is the opposite direction.
- Nothing would rely on it. Purely additive. OAuth and CLI keep their existing priority; this fills the gaps between polls. If it never fires, behavior is exactly what it is today.
The argument against, which I don't think is weak: the feature still occupies a user-configurable settings slot, and slot ownership is plausibly half of why the rule exists. That concern is real and survives points 1–3.
I don't think I should be the one to resolve that. If you read it broadly, say so and I'll close this.
Proposed shape (if you're open to it)
Opt-in, default off, and removable:
- Transport: file drop, no listener. A shim writes the payload atomically to
~/Library/Application Support/CodexBar/ with 0600; the app watches via DispatchSource. No port, no bearer token, no network surface. I originally sketched a loopback HTTP listener mirroring orca's design, but that would have meant lifting CLILocalHTTPServer out of CodexBarCLI — which is an executableTarget (Package.swift:81) and therefore unimportable from the app — touching CLIServeCommand.swift and its test suite for no benefit. A listening socket in a menu bar app also isn't worth the scrutiny when a file achieves the same trust model.
- Never clobber the slot.
statusLine is a single slot, not a list. Detect three states — managed / user-owned / empty-after-prior-install — and treat "empty after we installed once" as an opt-out rather than an invitation to reinstall. Orca's hook-settings.ts:138-186 handles exactly this and is worth mirroring.
- Additive priority. Slots into the planner ahead of
.cli, behind .oauth.
- Clean uninstall that removes both the shim and the settings entry.
Design constraints I'd want your view on
These are the parts where I think the real work is, and where I'd most likely guess wrong:
- The live snapshot is partial. The statusline payload carries only
five_hour and seven_day. It has no seven_day_sonnet / seven_day_opus, no limits[].weekly_scoped, no seven_day_routines / cowork, no extra_usage, and no identity or plan fields — all of which OAuth maps today (docs/claude.md:79-90). A live-fed card would move its two main bars while those rows sit frozen at their last OAuth values. The merge semantics across sources are the actual design problem here, not the parser.
- Multi-account attribution. The payload carries no account identity — at best
CLAUDE_CONFIG_DIR. claude-swap switches accounts within the same config dir, so a session started before a swap would post the previous account's numbers with nothing to distinguish them. Token-based accounts have no config dir at all. Orca drops posts whose configDir doesn't match the last auth snapshot (service.ts:1455-1471); something equivalent seems necessary here, and it interacts with the provider-siloing rule in AGENTS.md:46.
- Two
.auto decision sites. docs/claude.md:12-15 and 27-29 note the lower-level direct fetcher still has its own .auto order pending the refactor. A fifth source probably needs to land in both, or explicitly in only one — your call which.
- Precedence when the user stops coding. At what age does a live snapshot lose to a fresh poll, and should a recent live value suppress a poll at all?
Prior art
stablyai/orca (MIT) ships this technique — src/shared/claude-statusline-rate-limits.ts, src/main/claude/statusline-script.ts, src/main/claude/hook-settings.ts. Their code comment on why they built it is the same diagnosis as above: "the endpoint 429s under Orca's polling." They kept their OAuth fetcher and layered this on top rather than replacing it.
Note the difference in position, because it cuts both ways: orca owns the sessions it feeds from, so it only sees its own panes. CodexBar would see every Claude Code session on the machine — broader coverage, but it also means CodexBar is writing a settings file for sessions it doesn't own. That asymmetry is worth weighing.
What I'm asking
- Does
AGENTS.md:47 forbid this, reading it as you intended it?
- If not — is the file-drop shape right, or would you rather it went in differently?
If both are green I'll send a draft PR with tests. If it's a no, no hard feelings — closing this is a perfectly good outcome and I'd rather know now.
Summary
Claude is the only provider whose live sources are both budget-gated, which makes its card structurally stale rather than occasionally wrong. Claude Code ≥ 2.1.80 exposes a server-authoritative
rate_limitsobject that costs nothing to read, but the only channel it comes through is thestatusLinecommand — andAGENTS.md:47says never to rely on the status line for usage parsing.I think there's a real distinction between what that rule forbids and what this would do, but it's your rule and your call. I'd rather get a ruling before writing code than send you a surprise PR that contradicts a documented line. Happy to implement whichever way you want it, or drop it.
The problem is staleness, not blanking
#2247 (
ff52c6d) fixed the blanking half of #2241 — transient parse failures now preserve the last-good snapshot and its timestamp. That part works.What's left is freshness, and it's structural rather than a parser bug. Both live sources carry the same 5-minute cooldown:
ClaudeOAuthUsageRateLimitGate.swift:6—defaultCooldown: TimeInterval = 60 * 5, keyed per access token, armed whenever/api/oauth/usage429sClaudeCLIRateLimitGate.swift:5— same cooldown for the PTY pathBoth gates are correct given what they're gating.
api.anthropic.com/api/oauth/usagehas a tight budget and 429s under polling; the PTY path pays a subprocess plus session-artifact cleanup per probe. The issue history is what that looks like from the outside: #1679 (rate limited), #2117 (slow refresh), #2251 (session-list pollution, since mitigated by theClaudeProbeartifact cleanup), and the 8.6% probe success rate measured in #2241.CodexBar has no Claude source that is cheap enough to read often. Every other fix is scheduling against a fixed budget.
The one free channel
Claude Code ≥ 2.1.80 passes a JSON object on stdin to whatever
statusLinecommand is configured. It contains:{ "rate_limits": { "five_hour": { "used_percentage": 42.7, "resets_at": 1786099200 }, "seven_day": { "used_percentage": 63.1, "resets_at": 1786608000 } } }This is piggybacked on Messages API responses the session already made, so reading it spends nothing against the usage endpoint's budget. It appears only for Claude.ai-subscriber sessions, and only after the session's first API response.
I checked whether it lands anywhere on disk first, because a file read would need none of this machinery. It doesn't:
~/.claude/projects/*/*.jsonlsession transcripts~/.claude.jsonbeyond a GrowthBook promo string that happens to containseven_daystats-cache.jsondoesn't carry it eitherCorroborating evidence that the hook surface doesn't expose it: orca (MIT) installs the full Claude hook suite — SessionStart, UserPromptSubmit, Stop, PreToolUse — and still had to build a separate statusline route to get
rate_limits. If any hook payload carried it, they wouldn't have.So the statusline slot appears to be the only channel, which is inconvenient given the rule.
The AGENTS.md:47 question
The argument that this proposal doesn't violate it:
ClaudeStatusProbescrapes rendered terminal output today —docs/claude.md:195-197: "Strips ANSI, locates 'Current session' + 'Current week' headers." That is the fragile pattern the rule describes. This is the opposite direction.The argument against, which I don't think is weak: the feature still occupies a user-configurable settings slot, and slot ownership is plausibly half of why the rule exists. That concern is real and survives points 1–3.
I don't think I should be the one to resolve that. If you read it broadly, say so and I'll close this.
Proposed shape (if you're open to it)
Opt-in, default off, and removable:
~/Library/Application Support/CodexBar/with0600; the app watches viaDispatchSource. No port, no bearer token, no network surface. I originally sketched a loopback HTTP listener mirroring orca's design, but that would have meant liftingCLILocalHTTPServerout ofCodexBarCLI— which is anexecutableTarget(Package.swift:81) and therefore unimportable from the app — touchingCLIServeCommand.swiftand its test suite for no benefit. A listening socket in a menu bar app also isn't worth the scrutiny when a file achieves the same trust model.statusLineis a single slot, not a list. Detect three states — managed / user-owned / empty-after-prior-install — and treat "empty after we installed once" as an opt-out rather than an invitation to reinstall. Orca'shook-settings.ts:138-186handles exactly this and is worth mirroring..cli, behind.oauth.Design constraints I'd want your view on
These are the parts where I think the real work is, and where I'd most likely guess wrong:
five_hourandseven_day. It has noseven_day_sonnet/seven_day_opus, nolimits[].weekly_scoped, noseven_day_routines/cowork, noextra_usage, and no identity or plan fields — all of which OAuth maps today (docs/claude.md:79-90). A live-fed card would move its two main bars while those rows sit frozen at their last OAuth values. The merge semantics across sources are the actual design problem here, not the parser.CLAUDE_CONFIG_DIR.claude-swapswitches accounts within the same config dir, so a session started before a swap would post the previous account's numbers with nothing to distinguish them. Token-based accounts have no config dir at all. Orca drops posts whoseconfigDirdoesn't match the last auth snapshot (service.ts:1455-1471); something equivalent seems necessary here, and it interacts with the provider-siloing rule inAGENTS.md:46..autodecision sites.docs/claude.md:12-15and27-29note the lower-level direct fetcher still has its own.autoorder pending the refactor. A fifth source probably needs to land in both, or explicitly in only one — your call which.Prior art
stablyai/orca (MIT) ships this technique —
src/shared/claude-statusline-rate-limits.ts,src/main/claude/statusline-script.ts,src/main/claude/hook-settings.ts. Their code comment on why they built it is the same diagnosis as above: "the endpoint 429s under Orca's polling." They kept their OAuth fetcher and layered this on top rather than replacing it.Note the difference in position, because it cuts both ways: orca owns the sessions it feeds from, so it only sees its own panes. CodexBar would see every Claude Code session on the machine — broader coverage, but it also means CodexBar is writing a settings file for sessions it doesn't own. That asymmetry is worth weighing.
What I'm asking
AGENTS.md:47forbid this, reading it as you intended it?If both are green I'll send a draft PR with tests. If it's a no, no hard feelings — closing this is a perfectly good outcome and I'd rather know now.