[BUG] Keychain and ~/.claude/.credentials.json diverge: orphaned plaintext credential causes false-healthy diagnosis

Status Open
Maintainer reply None cached
Activity 0 comments · opened Sep 1, 2026

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?

  1. When the client authenticates via Keychain, any stale ~/.claude/.credentials.json

should be removed or reconciled — an orphaned live refresh token should not persist.

  1. claude auth status should report which store the credential was read from, so

divergence is visible rather than silent.

  1. 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):

  1. 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()))"
`
Observed:
['mcpOAuth'] — no claudeAiOauth`.

  1. Inspect the plaintext store:

``
python3 -c "import json,os;print(sorted(json.load(open(os.path.expanduser('~/.claude/.credentials.json'))).keys()))"
`
Observed:
['claudeAiOauth'], unexpired, 5 scopes, subscriptionType max`.

  1. 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 restore
user:sessions:claude_code and will not fix claude --teleport. claude auth login
--claudeai
is the correct remedy.

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).

View original on GitHub ↗