Skip to content

fix(usage): read keychain credential by account, not service name alone - #532

Open
vishnujayvel wants to merge 1 commit into
sirmalloc:mainfrom
vishnujayvel:fix/keychain-multi-account
Open

fix(usage): read keychain credential by account, not service name alone#532
vishnujayvel wants to merge 1 commit into
sirmalloc:mainfrom
vishnujayvel:fix/keychain-multi-account

Conversation

@vishnujayvel

Copy link
Copy Markdown
Contributor

Problem

On macOS, all usage widgets can show [No credentials] even when a valid Claude Code OAuth token is in the keychain. The login keychain may hold more than one generic-password item under the service Claude Code-credentials, differing only by account:

service account contents
Claude Code-credentials <os-username> { mcpOAuth, claudeAiOauth } ← real token
Claude Code-credentials unknown { mcpOAuth } only

security find-generic-password -s "Claude Code-credentials" -w (no -a) returns whichever item security matches first — which can be the one lacking claudeAiOauth — so the token appears missing. The existing suffixed-service fallback can't recover it (it excludes the plain service name, and the Claude Code-credentials-<hex> services are all MCP tokens without claudeAiOauth).

Fixes #521.

Fix

getUsageToken() now, when the service-only read yields no claudeAiOauth, does one additional read scoped to the OS username — security find-generic-password -s "Claude Code-credentials" -a <os.userInfo().username> -w — mirroring how Claude Code itself writes/reads the item (-a <username> -s "Claude Code-credentials"). It sits between the existing service-only read and the hashed-candidate + .credentials.json fallbacks.

Resolution order is unchanged for single-item keychains (the service-only read still wins first), so this is a no-op on machines that already work. os.userInfo() is wrapped so a machine where it throws (uid absent from passwd) simply skips the new read. The suffixed-candidate scan and the .credentials.json fallback are untouched.

Scope / limitation

This resolves the observed case where the token lives under the OS-username account (which is what Claude Code writes). It intentionally does not try to guess a non-username account string (email/UUID/hash) — that would be speculative, and it can't regress those keychains (they already fall through to the existing fallbacks). If per-account resolution from config lands (cf. #219 / #266), the account could instead come from there; happy to align.

Why not reverse-engineer the suffixed service names

The many Claude Code-credentials-<hex> entries are per-MCP-server OAuth tokens, not account credentials — matching against them is a dead end (the existing candidate scan already can't help). Scoping by account is exactly what Claude Code does, so it stays correct if the suffix scheme ever changes.

Tests

  • reads the OS-username account when the service-only lookup returns an item without an OAuth token — drives the exact bug: service-only read returns an MCP-only item, -a <username> read returns the real token; asserts the precise security call sequence.
  • skips the OS-username account read when the username cannot be resolvedos.userInfo() throws → no -a lookup issued, chain proceeds to the fallbacks.
  • Existing getUsageToken sequence tests updated for the new -a <username> call.
  • On a branch merged with current main: bun test green (793 pass across 54 files, 0 fail) and bun run lint clean.

Verification

Reproduced live on an affected machine: before, all usage widgets rendered [No credentials]; after, the token resolves via the OS-username account and the usage API returns real values.

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.

All usage widgets show [No credentials] on macOS when the login keychain has multiple Claude Code-credentials items (different accounts)

1 participant