Files
lumbridge-code/docs/decisions/0016-reading-the-claude-code-credential.md
T
Metal AgentandClaude Opus 5 219c674aea Read Claude Code's own quota endpoint, not just its status line
Decision 0015 rejected the account usage endpoint because AGENTS.md forbade
reading a harness's credential. The rule was written to stop one program
helping itself to another's secrets, and it was catching a legitimate use with
it: the user asking about their own subscription, through software they
installed to do that. AGENTS.md now states the narrow allowance instead of an
absolute the project does not hold, and 0016 records it.

The status line stays. It is free and it speaks every turn. What it cannot do
is report the per-model weekly limits a Max plan meters separately, or answer
at all before a session has taken a turn. The first live reading found the
account-wide seven-day window at 38% left and a per-model weekly window at 77%
left — a second ceiling the footer previously could not see.

Constraints the credential is read under, all enforced in code: access token
only, never the refresh token; zeroed on drop, along with the file buffer it
was borrowed out of; unprintable by construction, since HarnessError carries no
owned strings and AccessToken's Debug is hand-written; identified as
lumbridge/<version>, because sending claude-code/2.1.0 would make our traffic
indistinguishable from the harness's in Anthropic's logs; and off entirely
under LUMBRIDGE_CLAUDE_OAUTH=0.

The request runs on a detached thread with a slow refresh and a 429 backoff, so
a ten-second round trip cannot stall the transcript follower or make quitting
wait on the network, and one surface failing does not fault the other two.

Footer polish on top: the harness name prints once per group instead of in
front of each of its four windows, each quota carries a short scope pill
(5h, 7d, Fable wk, tokens) where an invisible BORDER-weight label used to be,
quotas sort ahead of spend, and a window under ten percent turns its headline
amber — value colour on the number, provenance colour on the meter, never
mixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 22:04:34 -07:00

4.8 KiB

0016: Lumbridge reads Claude Code's credential to ask about the user's own quota

Status: accepted; verified against a live account. Supersedes the rejection recorded in decision 0015 and narrows the blanket prohibition in AGENTS.md.

What changed

Decision 0015 chose the status line and recorded the account usage endpoint as rejected, because AGENTS.md said Lumbridge "must not scrape their private credentials." That was the right reading of the rule. The rule was too broad.

The prohibition exists to stop one program helping itself to another's secrets: exfiltration, impersonation, using a credential for something its owner did not intend. Reading the token Claude Code stored on this machine, to ask Anthropic how much of this user's subscription is left, is none of those things. It is the user asking about their own account through software they installed for that purpose. The rule was written to prevent an abuse and was catching a legitimate use with it, so AGENTS.md now states the narrow allowance instead of an absolute that the project does not actually hold.

Why the status line was not enough

It was a real improvement and it stays — it is free and it updates every turn. Two things it cannot do:

  • Per-model weekly limits. A Max plan meters some models separately, and those arrive only in the endpoint's limits[] as weekly_scoped entries. The first live reading against a real account showed the account-wide seven-day window at 38% left and a per-model weekly window at 77% left — a second ceiling that the status line has no field to report. A footer that cannot see it will say a user has room when the limit that stops them is a different one.
  • Answering on a cold start. The status line speaks only while Claude Code is taking turns. Open Lumbridge in the morning and it has nothing to say until you start a session, which is exactly when you want to know.

So both surfaces run. The endpoint is the authority whenever it answers, because it is the account's own statement rather than a header relayed through a session that may have ended hours ago; the status line covers the interval between refreshes for free. Both are ProviderReported, and they share the window arithmetic so the same quota read two ways cannot produce two numbers.

The constraints this is allowed under

  • The access token only. The credential file also holds a refresh token. Refreshing would mean writing to another program's credential store, so an expired token is reported as an expected gap and Claude Code renews it on its next turn.
  • Never persisted, logged, or in argv. HarnessError is Copy and carries only static strings, numbers, and io::ErrorKind, so no error can capture the token even by accident. AccessToken has a hand-written Debug that prints <redacted>, and both it and the file buffer are zeroed on drop. That last part is a real but partial guarantee: the token is borrowed out of the file buffer rather than copied through an intermediate String, so the only two copies are the ones this module owns and clears — but nothing here defends against the operating system having paged either buffer out.
  • No impersonation. The request identifies itself as lumbridge/<version> (usage-probe). Sending claude-code/2.1.0, as other tools do, would make Lumbridge's traffic indistinguishable from the harness's in Anthropic's own logs. The product boundary forbids that, and no rate limit is worth being unable to tell who made a request.
  • The e-mail address is not read. It sits in ~/.claude.json beside what we do read. The plan tier — Max (20x) — is taken from the credential file because it names the quota being metered; the account identity is not needed to render a percentage.
  • One switch off. LUMBRIDGE_CLAUDE_OAUTH=0 means the credential file is never opened. A user who wants the status-line reading and nothing else has that, and the footer degrades to exactly what decision 0015 shipped.

Rate limiting

The endpoint 429s under polling. Refreshes are five minutes apart at the most frequent, back off to thirty after a refusal, and run on a detached thread — a ten-second request must not stall the transcript follower, and joining one would make quitting Lumbridge wait on a network round trip.

A failed request does not fault the probe. Three surfaces report here; one going quiet must not hide the two that are still working.

Adapted from

The request shape — GET /api/oauth/usage with anthropic-beta: oauth-2025-04-20 — and the weekly_scoped reading are the documented behaviour of Claude Code, observed in the bb and orca reference implementations under Research/agent-ides/. No implementation code was copied; the credential path, response fields, and status handling were read as protocol documentation. The User-Agent deliberately differs.