Stored /login credentials silently beat a valid CLAUDE_CODE_OAUTH_TOKEN (contradicts documented auth precedence)

Open 💬 4 comments Opened Jun 22, 2026 by chrisvaillancourt

Stored /login credentials silently beat a valid CLAUDE_CODE_OAUTH_TOKEN

Summary

When the macOS Keychain holds /login credentials, Claude Code authenticates with them even when the environment sets a valid CLAUDE_CODE_OAUTH_TOKEN. The documented precedence ranks the env token (#5) above subscription /login credentials (#6): https://code.claude.com/docs/en/authentication#authentication-precedence

Claude Code shows no warning. A session meant for account A silently runs — and bills usage — against account B.

Environment

  • macOS (Darwin 25.5.0), native installer
  • Reproduced identically on 2.1.173, 2.1.174, and 2.1.175; still unfixed on 2.1.185 (latest on main as of this writing)
  • Two Claude subscription accounts: account A (logged in via /login, creds in the Keychain) and account B (1-year token from claude setup-token)

Steps to reproduce

# Keychain holds account-A /login creds. $TOKEN_B is a valid account-B token
# from `claude setup-token`.

run() {
  CLAUDE_CODE_ENABLE_TELEMETRY=1 OTEL_METRICS_EXPORTER=console OTEL_METRIC_EXPORT_INTERVAL=2000 \
    "$@" claude -p "Reply with just: ok" --model claude-haiku-4-5-20251001 2>&1 | \
    grep -o 'account_uuid[^,}]*' | sort -u
}

run env -u CLAUDE_CODE_OAUTH_TOKEN                                          # A: keychain only
run env CLAUDE_CODE_OAUTH_TOKEN="$TOKEN_B"                                  # B: valid token + keychain
run env CLAUDE_CODE_OAUTH_TOKEN="$TOKEN_B" CLAUDE_CONFIG_DIR=/tmp/empty     # C: valid token, no stored creds

Results

| # | Stored creds | Env token | Account used |
|---|---|---|---|
| A | account-A keychain | unset | A (baseline) |
| B | account-A keychain | valid account-B | A ← bug; docs say the token wins |
| C | none | valid account-B | B (correct) |
| D | account-A keychain | invalid garbage | A — silent fallback, no warning |
| E | none | invalid garbage | 401 Invalid bearer token (correct) |

A and B return the identical account_uuid (account A). C succeeds with the account-B token, proving the token is valid and usable. api.anthropic.com/api/oauth/profile confirms the token is non-expired: it returns a 403 scope error for user:profile, so authentication succeeds and the token is inference-scoped — not a 401.

Expected

Per the documented precedence, case B authenticates with the env token (account B). Failing that, Claude Code warns whenever both credential sources exist and it picks the lower-ranked one.

The behavior looks intentional, and it contradicts the docs

Two changelog entries clear the env token so disk credentials win — the reverse of the documented precedence:

  • 2.1.118 — "Fixed /login having no effect in a session launched with CLAUDE_CODE_OAUTH_TOKEN — the env token is now cleared so disk credentials take effect"
  • 2.1.117 — "Fixed /login having no effect when launched with CLAUDE_CODE_OAUTH_TOKEN env var and that token expires"

These fixes target the /login flow, but the same env-token-cleared logic likely explains why a valid env token never wins at startup. The code and the docs disagree; one of them is wrong.

Related: #16238 (closed not-planned) framed the env token as the winner, matching the docs. Current behavior is the reverse.

Symptom that led here

Interactive sessions launched with the account-B token briefly showed account-B rate-limit state at startup, then flipped to account A's limits after the first message — usage silently billed to the wrong account.

Workaround

Tokens-only auth: /logout the Keychain account, then switch both accounts through per-shell CLAUDE_CODE_OAUTH_TOKEN exports. Cost: Remote Control stops working, because setup-token is inference-only and Keychain creds cannot coexist without re-triggering the bug.

Per-account CLAUDE_CONFIG_DIR is a partial alternative, but the docs specify config-dir credential isolation only via .credentials.json on Linux and Windows. On macOS the creds live in the Keychain, so per-config-dir isolation is unverified, and it fragments plugins, skills, agents, commands, and MCP config across directories.

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗