[BUG] Keychain and ~/.claude/.credentials.json diverge: orphaned plaintext credential causes false-healthy diagnosis
What's Wrong?
On macOS, Claude Code reads OAuth credentials from the Keychain, but a plaintext~/.claude/.credentials.json can persist containing a complete, unexpired credential
that the client never uses for authentication. The two stores diverge silently.
Result: the client correctly reports "Not logged in", while a file on disk shows a valid
Max subscription credential with full scopes. Every human and every tool that inspects
that file concludes authentication is fine.
Observed on this machine:
Keychain "Claude Code-credentials" top-level keys: ['mcpOAuth']
claudeAiOauth: ABSENT
~/.claude/.credentials.json top-level keys: ['claudeAiOauth']
subscriptionType: max
scopes: user:file_upload, user:inference,
user:mcp_servers, user:profile,
user:sessions:claude_code
expiresAt: unexpired
refreshTokenExpiresAt: +27 days
accessToken: present (108 chars)
Two distinct problems follow:
1. Diagnosability. The plaintext file is the obvious place to look and it reports a
false pass. This cost roughly six hours of misdirected debugging, during which the
correct advice ("run /login") was actively avoided because the file looked healthy.
2. Security. An orphaned plaintext file retains a live refresh token with 27 days of
validity that the client will never use, never refresh, and never clean up. A Keychain
item requires per-application ACL consent; a 0600 file requires only executing as the
user. The credential outlives its own usefulness in the weaker of the two stores.
What Should Happen?
- When the client authenticates via Keychain, any stale
~/.claude/.credentials.json
should be removed or reconciled — an orphaned live refresh token should not persist.
claude auth statusshould report which store the credential was read from, so
divergence is visible rather than silent.
- When no usable credential is found in the Keychain but a credential exists in the
plaintext file, say so explicitly. "Not logged in" is accurate but gives the user no
path to the actual problem.
Error Messages/Logs
$ claude --debug-to-stderr -p "say ok"
[DEBUG] [Bootstrap] Skipped: no usable OAuth, WIF, or API key
[ERROR] API error (attempt 1/11): Could not resolve authentication method.
Expected one of apiKey, authToken, credentials, config, or profile to be set.
Not logged in · Please run /login
Simultaneously, the startup banner renders Welcome back <name> and the account's
organization as claude_max, sourced from oauthAccount in ~/.claude.json — a third
store that still holds the profile. Three stores, three different answers.
Steps to Reproduce
Exact trigger for the divergence is not established. Observed state after normal use
(the machine was authenticated and working the previous day):
- Inspect the Keychain item the client reads:
```
security find-generic-password -s "Claude Code-credentials" -w \
| python3 -c "import sys,json;print(sorted(json.load(sys.stdin).keys()))"
['mcpOAuth']
Observed: — no claudeAiOauth`.
- Inspect the plaintext store:
```
python3 -c "import json,os;print(sorted(json.load(open(os.path.expanduser('~/.claude/.credentials.json'))).keys()))"
['claudeAiOauth']
Observed: , unexpired, 5 scopes, subscriptionType max`.
- Run any command requiring auth. Observe "Not logged in" despite step 2.
Additional Information
Note on claude setup-token as a workaround: the binary states these tokens are
"limited to inference-only for security reasons", so they do not restoreuser:sessions:claude_code and will not fix claude --teleport. claude auth login is the correct remedy.
--claudeai
Related: #91158 (this issue was split out of a correction to that report; the
"client misreports auth mode" claim there is withdrawn in favor of this).