fix(usage): read keychain credential by account, not service name alone - #532
Open
vishnujayvel wants to merge 1 commit into
Open
fix(usage): read keychain credential by account, not service name alone#532vishnujayvel wants to merge 1 commit into
vishnujayvel wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 serviceClaude Code-credentials, differing only by account:Claude Code-credentials<os-username>{ mcpOAuth, claudeAiOauth }← real tokenClaude Code-credentialsunknown{ mcpOAuth }onlysecurity find-generic-password -s "Claude Code-credentials" -w(no-a) returns whichever itemsecuritymatches first — which can be the one lackingclaudeAiOauth— so the token appears missing. The existing suffixed-service fallback can't recover it (it excludes the plain service name, and theClaude Code-credentials-<hex>services are all MCP tokens withoutclaudeAiOauth).Fixes #521.
Fix
getUsageToken()now, when the service-only read yields noclaudeAiOauth, 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.jsonfallbacks.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.jsonfallback 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 precisesecuritycall sequence.skips the OS-username account read when the username cannot be resolved—os.userInfo()throws → no-alookup issued, chain proceeds to the fallbacks.getUsageTokensequence tests updated for the new-a <username>call.main:bun testgreen (793 pass across 54 files, 0 fail) andbun run lintclean.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.