Skip to content

StatusLine: usage meters silently lose the scoped-model pill on a multi-item keychain - #2220

Open
theNetworkChuck wants to merge 2 commits into
danielmiessler:mainfrom
theNetworkChuck:fix/statusline-usage-silent-auth-failure
Open

theNetworkChuck wants to merge 2 commits into
danielmiessler:mainfrom
theNetworkChuck:fix/statusline-usage-silent-auth-failure

Conversation

@theNetworkChuck

@theNetworkChuck theNetworkChuck commented Sep 22, 2026

Copy link
Copy Markdown

What's wrong

On macOS the keychain can hold more than one item under the service name Claude Code-credentials, told apart by account. Claude Code 2.1.278 leaves a legacy acct="claude-code-user" item whose accessToken and refreshToken are empty strings, beside the live acct="$USER" item it refreshes every session:

"acct"="claude-code-user"   mdat=20260918135711Z   "svce"="Claude Code-credentials"   <- accessToken: ""
"acct"="<user>"             mdat=20260922084730Z   "svce"="Claude Code-credentials"   <- the live one

security find-generic-password -s "Claude Code-credentials" -w names only the service, so it returns whichever item comes back first. Where that is the stub, every /api/oauth/usage fetch authenticates with an empty bearer token and fails.

Why nobody notices

The cache never lands, _data_age passes USAGE_HARD_EXPIRY, and the native-payload branch stops enriching. The scoped per-model pill and the EXT indicator are then simply not drawn — while 5H and WK keep rendering from the stdin rate_limits object, which carries only five_hour and seven_day.

So the status line looks perfectly healthy. An omitted pill is indistinguishable from an account that has no scoped window at all. On the install where this was found, the fetch had been dead for four days.

The two commits

1. Pick the keychain item that actually holds a token. Probe the cached winner, then $USER, then claude-code-user, then the bare lookup, and take the first item carrying a non-empty token. If every one comes back empty the layout has moved again, so enumerate the items under that service and try each — one dump-keychain, only on a layout change, because the winning account is cached in /tmp/pai-kc-acct-$USER. A fetch that returns nothing usable clears that cache so the next render re-probes rather than retrying a dead credential.

2. Show a dead cache instead of hiding it. Past USAGE_HARD_EXPIRY, emit the model name from the expired cache and hold the scoped slot with a dimmed FB -. No percentage: a stale number a reader could mistake for live data is worse than no number. Fresh caches are untouched.

Verification

Keychain lookup, same machine:

-s only            -> accessToken length 0   (the stub)
-s with -a $USER   -> accessToken length 108 -> /api/oauth/usage http=200, Fable 64%

Forced the fallback by running with a bogus $USER and no cached hint, so every known account returned empty. It enumerated, found the live item, and rendered the pill.

Rendering, native payload on stdin against a cache aged past hard expiry:

stock    5H  52% 1030   WK  49% SUN@0300         SUB
patched  5H  52% 1030   WK  49% SUN@0300  FB -   SUB

Linux is unaffected — that branch reads ~/.claude/.credentials.json and is untouched. bash -n clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_015V34cQV19jCGFpsEbFosL4

On macOS the keychain can hold more than one item under the service name
"Claude Code-credentials", told apart by account. Claude Code 2.1.278
leaves a legacy acct="claude-code-user" item whose accessToken and
refreshToken are empty strings beside the live acct="$USER" item it
refreshes each session.

`security find-generic-password -s "Claude Code-credentials" -w` names
only the service, so it returns whichever item the keychain yields first.
Where that is the stub, every /api/oauth/usage fetch authenticates with an
empty bearer token and fails.

The failure is silent. The cache never lands, \_data\_age passes
USAGE_HARD_EXPIRY, and the native-payload branch simply stops enriching:
the scoped per-model pill and the EXT indicator vanish while 5H and WK keep
rendering from stdin, so the line looks healthy. Observed in the wild for
four days before anyone noticed the missing pill.

Probe the cached winner, then $USER, then claude-code-user, then the bare
lookup, and take the first item carrying a non-empty token. If all of them
come back empty the layout has moved again, so enumerate the items under
that service and try each; that costs one dump-keychain and only on a
layout change, because the winning account is cached in
/tmp/pai-kc-acct-$USER. A fetch that returns nothing usable clears that
cache so the next render re-probes instead of retrying a dead credential.
When the native rate_limits payload supplies 5H and WK, the scoped per-model
window and the EXT indicator still come from the OAuth cache. Past
USAGE_HARD_EXPIRY that enrichment is skipped, and the scoped pill is simply
not drawn.

An omitted pill is indistinguishable from an account that has no scoped
window at all, so a fetch that has been failing for days renders as a
perfectly healthy status line. That is how the keychain bug in the previous
commit went unnoticed.

Hold the slot instead: emit the model name from the expired cache and draw a
dimmed 'FB -' in the scoped position. No percentage, because a stale number a
reader could mistake for live data is worse than no number at all. Fresh
caches are untouched.

Verified against a cache aged past hard expiry with a native payload on
stdin: stock renders '5H .. WK .. SUB', patched renders '5H .. WK .. FB - SUB'.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant