[BUG] macOS CLI login loop: Keychain credentials blob has every accessToken/refreshToken blanked (claudeAiOauth + all mcpOAuth entries)

Status Open
Reported on v2.1.222
Maintainer reply ✓ Yes — bcherny
Activity 3 comments · opened Aug 5, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Preflight Checklist

  • [x] I have searched existing issues — this is closely related to #83345 but on a different surface (CLI, not the VSCode extension) and with a broader corruption scope; filing separately per that difference, happy to have it merged.
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

Environment

  • macOS 26.5.2 (build 25F84), Darwin 25.5.0, arm64
  • Claude Code CLI 2.1.222, installMethod: native (~/.local/bin/claude~/.local/share/claude/versions/2.1.222)
  • Terminal: Terminal.app
  • Sign-in method: Claude account via browser OAuth
  • ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN: unset. No proxy, no Bedrock/Vertex, no NODE_EXTRA_CA_CERTS.

What's wrong

claude in the terminal is stuck in a permanent login loop. Completing the browser OAuth flow never results in an authenticated session — running claude again immediately asks to sign in. Repeating the flow any number of times changes nothing.

Root cause observed

The macOS Keychain item Claude Code-credentials (in login.keychain-db) exists and is readable, parses as valid JSON, and has all of its non-secret fields intact — but every secret field in the entire blob is an empty string:

{
  "claudeAiOauth": {
    "accessToken": "",              // EMPTY
    "refreshToken": "",             // EMPTY
    "expiresAt": <in the past>,     // EXPIRED
    "refreshTokenExpiresAt": <number present>,
    "subscriptionType": "<intact>",
    "rateLimitTier": "<intact>"
  },
  "mcpOAuth": {
    "<server-1>": { "accessToken": "", "serverName": "<intact>", "serverUrl": "<intact>",
                    "clientId": "<intact>", "redirectUri": "<intact>",
                    "discoveryState": { "oauthMetadataFound": true, ... } },
    // ... same shape for all 7 stored MCP servers
  }
}

All 7 mcpOAuth entries have accessToken: "" while their serverName, serverUrl, clientId, redirectUri, and discoveryState survived intact. One entry that stores a clientSecret retained a non-empty value for it.

This is the same corruption signature as #83345, but that report only observed the claudeAiOauth object via the VSCode extension. Here it affects every token-bearing field across the whole credential blob simultaneously, which suggests a write/serialization path that blanks token fields wholesale rather than an OAuth flow failing per-service.

Why this produces an infinite loop

  1. Startup finds the Keychain item present → does not report "no credentials", so no error surfaces.
  2. It reads accessToken: "" → cannot authenticate → prompts sign-in.
  3. Browser OAuth completes, but the credential is never successfully written back.
  4. Next launch repeats from step 1, forever.

Supporting evidence that step 3 is a silent write failure: the Keychain item's modification date (mdat) is still 2026-08-02T17:28:51Z, unchanged despite many sign-in attempts on 2026-08-05. A successful write would have advanced it. The item is writable and the containing keychain is unlocked — reading the item non-interactively returns exit code 0 with no ACL prompt, so this is not a locked-keychain case.

Expected vs actual

  • Expected: completing browser OAuth persists a valid token and the next claude run is authenticated.
  • Actual: the token is never persisted; the stale blob with empty tokens is retained indefinitely.

Suggested handling

  1. Validate on read. Treat a credential blob whose accessToken is empty/whitespace as absent rather than present, so the corrupt state can't masquerade as a valid entry.
  2. Verify the write. After OAuth, read the credential back and fail loudly if it did not persist, instead of reporting success. (#79407 reports the same class of silent-write problem from a locked keychain.)
  3. Investigate the blanking itself. That non-secret sibling fields survive while every token field across both claudeAiOauth and all mcpOAuth entries is emptied points at the serialization/merge step that rewrites the whole blob, not at the individual OAuth flows.

Related issues

  • #83345 — same empty-token Keychain corruption, VSCode extension, v2.1.220 (claudeAiOauth only)
  • #79407 — macOS: /login falsely reports success while the token write silently fails
  • #72875, #78946 — other reports of /login looping

Workaround

Deleting the corrupted item and re-authenticating clears the loop:

security delete-generic-password -s "Claude Code-credentials"

This also discards all stored MCP OAuth grants, so every MCP server must be re-authorized afterward — though in this state their tokens were already blank.

View original on GitHub ↗

3 Comments

bcherny collaborator · 15 days ago

Closing as a duplicate of #83345 — please follow and 👍 that issue instead so we can track it in one place. If you think this was closed in error, comment here and we'll reopen.

🤖 Generated with Claude Code

arturo-natella · 15 days ago

Not dupe because it didn't happen in VS code. Was trying to log in via CLI.

bcherny collaborator · 14 days ago

Reopened — thanks for the pushback, you're right that this isn't the same issue. Sorry for the noise.

🤖 Generated with Claude Code