Stored credentials silently override apiKeyHelper: requests 401 in an unrecoverable retry loop
Summary
When apiKeyHelper is configured (a command that prints an API token, used with ANTHROPIC_BASE_URL for gateway/proxy setups), Claude Code still prefers a stored credential — a claude auth login OAuth token, a previously "approved" API key in customApiKeyResponses.approved, or ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN — over the helper's output. Every request then sends the wrong credential to the configured base URL and 401s in an unrecoverable retry loop.
Environment
- Claude Code 2.1.x (latest at report date)
- macOS (also reproducible on Linux)
settings.json:apiKeyHelperset to a local command;env.ANTHROPIC_BASE_URL→ internal LiteLLM-compatible gateway- The helper returns a JWT the gateway accepts; the gateway rejects
sk-*virtual/API keys it didn't issue
Reproduction
- Configure
apiKeyHelper+ANTHROPIC_BASE_URLin~/.claude/settings.json. - Run
/logininside Claude Code (or have a previously approved API key /ANTHROPIC_API_KEYexported). Login reports success. - Send any message.
Expected: requests use apiKeyHelper's output (or Claude Code warns that a stored credential will take precedence / offers to clear it).
Actual: every request carries the stored sk-… credential to ANTHROPIC_BASE_URL; the server returns 401 authentication_failed; Claude Code retries ~10 times, then says "Please run /login" — which re-stores the same claude.ai credential and loops forever. The session is completely wedged; the only escape is claude /logout or manually deleting ~/.claude/.credentials.json / the Keychain entry.
Example server-side rejection (LiteLLM):
401 Invalid proxy server token passed. Received API Key = sk-…rAAA,
Key Hash = dc15f7… Unable to find token in cache or LiteLLM_VerificationTokenTable
Why it matters
The precedence order isn't visible anywhere: claude auth status reports the helper as the configured source while requests silently use the stored credential. Users can't tell the difference between "helper broken" and "stored credential shadowing helper", and the suggested recovery (/login) makes it strictly worse. A related ask — making auth login itself delegate to the helper — is #97770; this issue is the opposite edge: even when the helper is healthy, stored credentials override it.
Suggestions (any of)
- When
apiKeyHelperis set, ignore stored claude.ai OAuth tokens and approved custom keys for request auth (or make precedence explicit in docs andauth status). - Warn at startup when a stored credential conflicts with
apiKeyHelper: "a saved claude.ai credential overrides apiKeyHelper — runclaude /logout". claude auth statusshould show which credential will actually be sent, not just which source is configured.