Skip to content

Claude usage: opt-in live window feed from Claude Code's statusLine JSON — needs a ruling on AGENTS.md:47 #2733

Description

@luisgonzaleznf

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:6defaultCooldown: 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:

  1. 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.
  2. 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.
  3. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. Does AGENTS.md:47 forbid this, reading it as you intended it?
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal priority bug or improvement with limited blast radius.clawsweeper:needs-product-decisionClawSweeper marked this issue as needing a product or behavior decision.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.impact:auth-providerThis issue is about auth, provider routing, model choice, or SecretRef resolution.impact:ux-frictionUser-facing flow adds avoidable confusion or support burden without fully blocking progress.issue-rating: 🌊 off-meta tidepoolIssue quality rating does not apply to this item.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions