Summary
Setting CLAUDE_CODE_OAUTH_TOKEN as an environment variable causes the CLI to silently delete the macOS Keychain credential entry ("Claude Code-credentials") on process exit. This breaks authentication for all other Claude Code sessions (VS Code extension, other terminals).
Environment
- Claude Code v2.1.81
- macOS Darwin 25.3.0 (Apple Silicon)
- Cursor (VS Code fork) + Terminal.app
Steps to Reproduce
-
Confirm Keychain entry exists:
security find-generic-password -a "$USER" -s "Claude Code-credentials"
# → returns entry
-
Run a single claude -p with the env var:
TOKEN=$(security find-generic-password -a "$USER" -w -s "Claude Code-credentials" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['claudeAiOauth']['accessToken'])")
CLAUDE_CODE_OAUTH_TOKEN="$TOKEN" claude -p "ping" --model haiku
-
Check Keychain after:
security find-generic-password -a "$USER" -s "Claude Code-credentials"
# → "The specified item could not be found in the keychain."
Expected Behavior
When CLAUDE_CODE_OAUTH_TOKEN is set, the CLI should bypass Keychain (which it does for reads) without modifying or deleting existing Keychain entries. The env var is documented as a way to provide a pre-authenticated token — it should be read-only with respect to credential storage.
Actual Behavior
The CLI deletes the Keychain entry on exit. This causes:
- VS Code extension loses auth → shows "How do you want to log in?" screen
- Other terminal sessions lose auth
- Any concurrent
claude processes that rely on Keychain fail with 401
Source Code Analysis
From reverse-engineering cli.js v2.1.81:
-
When CLAUDE_CODE_OAUTH_TOKEN is set, the credential getter returns {accessToken: env.CLAUDE_CODE_OAUTH_TOKEN, refreshToken: null, expiresAt: null} — correctly bypassing Keychain reads
-
The credential store uses a fallback combiner (FI7): Keychain primary → plaintext fallback (~/.claude/.credentials.json). The combiner has cleanup logic: "successful write to backend A → delete from backend B"
-
The bug: During process lifecycle, some code path triggers a credential write to the plaintext file. The combiner's cleanup logic then fires: "successful plaintext write → delete Keychain entry." This deletion is incorrect when the env var is set, because:
- The process never needed Keychain in the first place
- Other processes/sessions depend on that Keychain entry
- The deletion is a destructive side effect of a read-only operation
Impact
This bug is particularly severe for batch workflows. Users running parallel claude -p processes (e.g., for classification tasks) who set CLAUDE_CODE_OAUTH_TOKEN to avoid the Keychain race condition (#24317) end up destroying the credentials that their VS Code extension session depends on.
It creates a catch-22:
Proposed Fix
When CLAUDE_CODE_OAUTH_TOKEN is set (or EH8() returns true for setup-token mode):
- Skip all credential writes (not just reads) — the env var token is ephemeral and shouldn't persist
- Skip the fallback combiner's cleanup logic entirely
- Never call
security delete-generic-password for the main credentials entry
Pseudocode:
// In the credential write path:
if (process.env.CLAUDE_CODE_OAUTH_TOKEN || isSetupTokenMode()) {
return; // Don't write, don't delete — env var mode is read-only
}
Workaround
We built a wrapper script that:
- Extracts the token from
~/.claude/.credentials.json (not Keychain)
- Passes
CLAUDE_CODE_OAUTH_TOKEN only to the child process
- Restores the Keychain entry after batch completion if deleted
#!/bin/bash
# claude-batch — wrapper for parallel claude -p without auth kickout
CREDS_FILE="$HOME/.claude/.credentials.json"
extract_token() {
python3 -c "
import json, sys
with open('$CREDS_FILE') as f:
data = json.load(f)
print(data['claudeAiOauth']['accessToken'])
"
}
restore_keychain() {
if security find-generic-password -a "$USER" -s "Claude Code-credentials" > /dev/null 2>&1; then
return 0 # already exists
fi
security add-generic-password -a "$USER" -s "Claude Code-credentials" \
-w "$(cat "$CREDS_FILE")" 2>/dev/null
}
TOKEN=$(extract_token) || exit 1
CLAUDE_CODE_OAUTH_TOKEN="$TOKEN" claude "$@"
Usage: claude-batch -p "prompt" --model sonnet instead of claude -p.
After batch: call restore_keychain.
Tested: 30 parallel calls from Cursor Bash tool, 30/30 success, extension auth preserved.
Related Issues
Summary
Setting
CLAUDE_CODE_OAUTH_TOKENas an environment variable causes the CLI to silently delete the macOS Keychain credential entry ("Claude Code-credentials") on process exit. This breaks authentication for all other Claude Code sessions (VS Code extension, other terminals).Environment
Steps to Reproduce
Confirm Keychain entry exists:
Run a single
claude -pwith the env var:Check Keychain after:
Expected Behavior
When
CLAUDE_CODE_OAUTH_TOKENis set, the CLI should bypass Keychain (which it does for reads) without modifying or deleting existing Keychain entries. The env var is documented as a way to provide a pre-authenticated token — it should be read-only with respect to credential storage.Actual Behavior
The CLI deletes the Keychain entry on exit. This causes:
claudeprocesses that rely on Keychain fail with 401Source Code Analysis
From reverse-engineering
cli.jsv2.1.81:When
CLAUDE_CODE_OAUTH_TOKENis set, the credential getter returns{accessToken: env.CLAUDE_CODE_OAUTH_TOKEN, refreshToken: null, expiresAt: null}— correctly bypassing Keychain readsThe credential store uses a fallback combiner (
FI7): Keychain primary → plaintext fallback (~/.claude/.credentials.json). The combiner has cleanup logic: "successful write to backend A → delete from backend B"The bug: During process lifecycle, some code path triggers a credential write to the plaintext file. The combiner's cleanup logic then fires: "successful plaintext write → delete Keychain entry." This deletion is incorrect when the env var is set, because:
Impact
This bug is particularly severe for batch workflows. Users running parallel
claude -pprocesses (e.g., for classification tasks) who setCLAUDE_CODE_OAUTH_TOKENto avoid the Keychain race condition (#24317) end up destroying the credentials that their VS Code extension session depends on.It creates a catch-22:
CLAUDE_CODE_OAUTH_TOKEN: Keychain race condition kicks out the extension (Frequent re-authentication required with multiple concurrent Claude Code sessions (OAuth refresh token race condition) #24317)CLAUDE_CODE_OAUTH_TOKEN: Keychain entry gets deleted, kicks out the extensionProposed Fix
When
CLAUDE_CODE_OAUTH_TOKENis set (orEH8()returns true for setup-token mode):security delete-generic-passwordfor the main credentials entryPseudocode:
Workaround
We built a wrapper script that:
~/.claude/.credentials.json(not Keychain)CLAUDE_CODE_OAUTH_TOKENonly to the child processUsage:
claude-batch -p "prompt" --model sonnetinstead ofclaude -p.After batch: call
restore_keychain.Tested: 30 parallel calls from Cursor Bash tool, 30/30 success, extension auth preserved.
Related Issues