MCP OAuth storage wiped mid-session (no update involved); CLI silently registers a new dynamic client, breaking auth for all remote MCP servers
Environment
- Claude Code 2.1.217 (macOS, darwin; embedded Node v26.3.0)
- Two remote HTTP MCP servers with OAuth:
https://mcp.hospitable.com/mcp(dynamic client registration) andhttps://mcp.frontapp.com/mcp(static client_id + client_secret configured viaclaude mcp add --client-id ... --client-secret) - Multiple concurrent interactive sessions (~6 different projects active in the window)
Summary
All stored MCP OAuth credentials (access tokens, refresh tokens, dynamic client registrations, AND a statically-configured client_secret) vanished from storage mid-session, with no CLI update involved — the same 2.1.217 binary was running before, during, and after. When the next session found storage empty, it silently registered a brand-new dynamic client and cached needs-auth, so the user just sees "Needs authentication" again with no indication credentials were lost. Over 8 days this produced 4 distinct dynamic client_ids registered against the same server.
We had previously attributed recurring auth loss to update-time OAuth storage wipes (observed 2026-07-01 on 2.1.197→2.1.198, and again ~2026-07-06). This incident shows storage can also be lost with no update, pointing at a concurrent read-modify-write race on the shared credential store.
Timeline (2026-07-22, UTC; all logs from ~/Library/Caches/claude-cli-nodejs/<proj>/mcp-logs-hospitable/)
- 04:08 — fresh interactive OAuth for hospitable completes: client
a251208c-adcc-4089-9e9c-0df9ff5c62ce,Token expires in: 43200,Has refresh token: true,Saving tokens. - 04:24–05:07 — sessions in 6 different projects connect successfully with that token ("Successfully connected" each time; token read fine at 05:07:13).
- 05:11:27 — a session in another project logs:
```
No token data found
No client info found
No access token in storage
Authorization URL: ...client_id=a2513799-a9b4-4a12-930a-32dda1e8a151...
a2513799...`), persisting it as the stored client info.
i.e. storage is completely empty **4 minutes after another session read it successfully**, with ~11 hours left on the access token — and the CLI silently performs a fresh dynamic client registration (
- 05:12:39 — the second OAuth server (Front, static client credentials) starts logging
Skipping connection (cached needs-auth)in a long-running session whose token refresh had succeeded at 00:21:26 ("Token refresh successful"). Its stored static client_secret is now missing (claude mcp get frontno longer shows "client_secret configured", while client_id survives in~/.claude.json). - 20:31 — still on 2.1.217 (User-Agent
claude-code/2.1.217), a new session finds "No access token in storage", "Found client info" = the self-registereda2513799, and reports needs-auth. - The 2.1.218 auto-update landed hours after the wipe — versions in User-Agent strings confirm 2.1.217 throughout the incident window.
Distinct defects
- Mid-session credential-storage loss. The entire MCP OAuth store (per-server tokens + client registrations + static client_secret) can be lost while sessions are running. The correlation with ~6 concurrent sessions suggests a last-writer-wins race: a process serializing the credentials blob from a stale in-memory copy (or treating a transient keychain read failure as "empty") clobbers everything other sessions saved.
- Silent re-registration masks the loss. When storage reads empty, the CLI immediately registers a new dynamic client and persists it. There is no warning that previously-stored credentials disappeared; the user only discovers auth is gone at next tool use. This also churns client registrations server-side — MCP providers see a stream of orphaned clients (6 from us in 8 days:
a242f8ce,a251208c,a2513799,a252fa71,a2530341,a253076f), and any refresh-token chain tied to the old client is dead. claude mcp loginclient reuse is inconsistent. With a valid registered client in storage (a2513799, self-registered at the 05:11 wipe), four consecutiveclaude mcp login hospitableattempts behaved differently: attempt 1 registered a new client (a252fa71), attempt 2 after "Authentication timeout" registered another (a2530341), attempt 3 reuseda2530341, attempt 4 registered yet another (a253076f). Whatever invalidates the stored registration compounds the orphan-client churn on the provider side.
Expected
- Concurrent sessions must not clobber each other's saved MCP OAuth credentials (per-server keys or file locking rather than whole-blob last-writer-wins).
- A keychain/storage read failure should be treated as an error (retry/surface), not as "no credentials exist".
- If stored credentials for a server disappear while a static client_id remains configured, surface a loud warning instead of silently re-registering / silently caching needs-auth.
claude mcp loginshould reuse a stored, still-valid client registration.
Impact
Every remote OAuth MCP server needs full interactive re-auth every few days; for servers with static client secrets the stored secret must be re-added from a password manager each time. We now run a SessionStart hook solely to detect these wipes.
4 Comments
Independent corroboration of defect 1 (storage loss) and 2 (silent re-registration / orphan
churn) from a different machine and a different set of MCP servers — Claude Code 2.1.220, macOS 26.6,
Node v22.14.0.
The whole store is dry, not just one server
Inspecting
~/.claude/.credentials.jsontoday:Both backups are timestamped 2026-07-22 15:27 — within a day of the 2026-07-22 incident window in
this issue. The live file has lost 3 entries relative to the backups and every surviving entry
has an empty
accessTokenand norefreshToken. The structural fields (clientId,clientSecret,redirectUri,discoveryState) survive; only the token material is gone.That last detail may narrow the cause: this doesn't look like a whole-blob clobber by a stale writer,
which would drop entries wholesale. It looks more like entries being rewritten *with the token fields
emptied* — consistent with a serializer writing a partially-populated in-memory record over a
fully-populated stored one.
Orphan churn, same signature
The 3 entries dropped from the live file were all for one server, and they show the re-registration
pattern described in defect 2 — three distinct
clientIds for a singleserverUrl:The middle one is a half-completed registration — client credentials persisted, no token, no expiry.
Same "registered but never finished" shape you describe.
Contributing factor on this machine
In my case the re-registration loop has a deterministic trigger, which may be useful for building a
repro: the provider's DCR response omits
grant_types/response_types, and the client throwsCannot read properties of undefined (reading 'map')after registration succeeds. So every retryis guaranteed to register a new client and then die before persisting a token — a reliable orphan
generator, no race required. Filed separately as #81661.
That doesn't explain the empty-token state of the other 61 entries, which points back at the storage
defect in this issue.
~/.claude/mcp-needs-auth-cache.jsonshows the needs-auth caching behaviour you describe, with asingle entry for the affected server and no indication that credentials were ever lost.
Caught this wipe in the act on macOS, and the trigger looks narrower than a concurrency race: the
mcpOAuthsection is emptied by the same write that rotates theclaudeAiOauthaccount token.Environment
installMethod: native(~/.local/bin/claude→~/.local/share/claude/versions/2.1.241)Claude Code-credentialsclaudeCLI sessions, several alive for daysMethod
A 30-second poller reading the keychain item and logging only sha256 prefixes and presence flags, never token values. One line per observed change.
The capture
Line 2 is the healthy control: an ordinary MCP token refresh rotates that one entry's
accessTokenandexpiresAtand touches nothing else.Line 3 is the wipe. The account token fingerprint changes and
mcpOAuthbecomes{}in the same write. The previousclaudeAiOauth.expiresAtwas1787584440553=2026-08-24T15:14:00Z, so this is the account token being refreshed at expiry.Why this looks like more than last-writer-wins
mcpOAuthis empty, not stale. A stale in-memory copy from any live process would still have carried that process's view of the entries, since the keychain item has held MCP entries continuously (cdat=2026-08-09T06:43:07Z, unchanged through every wipe) and the oldest running session started two days earlier. An empty map means the writer had no MCP section at all, not an old one.expiresAtchanges type. Every earlier value was an integer; the value written during the wipe is a float (1787613382524.616). A different serialization path produced that record.1787613382524=2026-08-24T23:16:22Z, so roughly an 8-hour TTL. With overnight sleep that lands as one wipe most mornings and sometimes a second later the same day, which is the pattern in this machine's logs (08-23T11:35then08-23T15:12;08-21T19:54then08-21T21:50).~/Library/Caches/claude-cli-nodejs/*/mcp-logs-*/, there is not one log line between2026-08-23T15:38:20Zand2026-08-24T08:00:24Z, yet the tokens were gone by the latter. No refresh attempt, no 401, no lock contention. Whatever erases them is not on the MCP connect or refresh path.Simultaneity across servers
Grepping every
mcp-logs-*directory for the loss markers, all four servers report it within the same second:Aftermath
By
15:58:46Zthe entries are back as tokenless stubs:accessToken: "", norefreshToken, noexpiresAt, withclientId/redirectUri/discoveryStatepresent. Two servers carry brand-newclientIds from fresh dynamic registrations, matching the orphan churn in this issue; the other two kept their prior registration. Those stubs then hit the self-sealing needs-auth loop described in #87405, which is why servers I do not manually re-authorize stay dead indefinitely whileclaude mcp liststill reports them healthy.Suggested area to look at
The account-token refresh path appears to write a credentials object built from
claudeAiOauthalone rather than mutating the stored blob, somcpOAuthis dropped rather than preserved. The three shipped fixes in this area (2.1.211 parallel-session logout after wake, 2.1.221 concurrent connector-token refresh, 2.1.225 keychain read timeout) are all present in 2.1.241 and do not cover this path.Happy to share the poller script, or to run a longer capture if a specific field would help.
Filed #92149 with a possibly-related data point for the account-token-rotation hypothesis in the 2026-08-24 comment above.
On 2.1.260 (macOS, native install), claude.ai connector auth never persists and each authorization writes a new top-level keychain service
Claude Code-credentials-<8-hex-hash>instead of updating one — 105 orphaned services accumulated over ~7 weeks, 1-3 per day. Each is modified ~1 second after creation and never read again.The bare
Claude Code-credentialsservice holding the account login updates in place correctly, and itsmdatadvances on the same days connector auth is lost, which is consistent withmcpOAuthbeing cleared by the write that rotatesclaudeAiOauth.Differs from #59460 in shape: those orphans live inside one keychain item under
mcpOAuth.<serverName>|<urlHash>; these are separate top-level services.Correcting my comment above: "connector auth never persists" was wrong. A cold-process
claude mcp list(session env markers stripped) lists all connectors as Connected, so credentials are persisted and readable. The keychain observation stands — 105 orphaned top-levelClaude Code-credentials-<hash>services, 1-3/day, each written once and never read — and the correlation with the account-tokenmdatadvancing on the same days still holds, which is the part relevant to this issue. #92149 has been retitled and rewritten accordingly.