All usage widgets show `[No credentials]` on macOS when the login keychain has multiple `Claude Code-credentials` items (different accounts)
Summary
On macOS, every usage widget (Session Usage, Weekly Usage, etc.) renders [No credentials] on some machines even though a valid Claude Code OAuth token is present in the keychain. The token is simply stored under a keychain item that ccstatusline's lookup doesn't select.
Root cause: the login keychain can contain more than one generic-password item under the service name Claude Code-credentials, differing only by account. ccstatusline reads with the service name only:
security find-generic-password -s "Claude Code-credentials" -wWith multiple items sharing that service, security returns whichever it matches first — which may be an item that has no claudeAiOauth (so parseUsageAccessToken returns null) — and ccstatusline never tries the other account.
How this happens
Claude Code itself always reads/writes the credential with an explicit account (the OS username):
security ... generic-password -a <os-username> -s "Claude Code-credentials" ...On an affected machine, the keychain ends up with (at least) two items:
| service | account | contents |
|---|---|---|
Claude Code-credentials |
<os-username> (e.g. vishnu) |
{ mcpOAuth, claudeAiOauth } ← the real token |
Claude Code-credentials |
unknown |
{ mcpOAuth } only ← no usage token |
The unknown-account item appears to be written by an MCP-OAuth code path that didn't resolve a username. security -s with no -a can return that one first → [No credentials], deterministically wrong on that machine.
(There may also be many Claude Code-credentials-<hex> suffixed services — these are per-server MCP OAuth tokens and do not contain claudeAiOauth; the existing dump-keychain fallback that scans suffixed services can't help here because it excludes the plain service name and none of the suffixed items hold the account token.)
Reproduce
Structure-only probe (does not print token values):
# Enumerate accounts under the plain service:
security dump-keychain 2>/dev/null \
| grep -B2 '"svce"<blob>="Claude Code-credentials"$' \
| grep '"acct"' | sort -u
# On an affected machine you'll see two accounts, e.g. "unknown" and "<your-username>".
# Which item has the token? (structure only — prints true/false, never the token)
has_token() { python3 -c 'import sys,json; print("has claudeAiOauth:", "claudeAiOauth" in (json.load(sys.stdin) or {}))'; }
security find-generic-password -s "Claude Code-credentials" -w | has_token # -> false (service-only match: MCP-only)
security find-generic-password -s "Claude Code-credentials" -a "$(id -un)" -w | has_token # -> true (OS-username account)Proposed fix
Make the keychain read account-aware, mirroring how Claude Code itself accesses the item:
- Keep the current service-only read (works on single-item keychains).
- If it yields no
claudeAiOauth, read-s "Claude Code-credentials" -a <os.userInfo().username>. - Existing suffixed-service and
.credentials.jsonfallbacks unchanged.
This is minimal, matches Claude Code's own access pattern (no fragile reverse-engineering), and is safe on machines with a single credential item (the new read only fires when the service-only read misses). It resolves the OS-username-account case observed here; it deliberately doesn't guess non-username account strings.
I have this fix implemented with unit tests and can open a PR if you'd like — flagging first since #219 and #266 also touch per-account/auth-scoping and I don't want to collide.
Environment
- macOS (Darwin arm64), Claude Code (native install)
- ccstatusline latest
main - Distinct from #236 (that was token-in-file-instead-of-keychain; here the token is in the keychain, just under a non-default account).
Source: sirmalloc/ccstatusline