MCP OAuth storage wiped mid-session (no update involved); CLI silently registers a new dynamic client, breaking auth for all remote MCP servers

Status Open
Reported on v2.1.217
Maintainer reply None cached
Activity 4 comments · opened Jul 23, 2026

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) and https://mcp.frontapp.com/mcp (static client_id + client_secret configured via claude 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...
`
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 (
a2513799...`), persisting it as the stored client info.

  • 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 front no 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-registered a2513799, 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

  1. 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.
  2. 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.
  3. claude mcp login client reuse is inconsistent. With a valid registered client in storage (a2513799, self-registered at the 05:11 wipe), four consecutive claude mcp login hospitable attempts behaved differently: attempt 1 registered a new client (a252fa71), attempt 2 after "Authentication timeout" registered another (a2530341), attempt 3 reused a2530341, 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 login should 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.

View original on GitHub ↗

4 Comments

mrpilot01 · 2 months ago

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.json today:

.credentials.json               62 mcpOAuth entries | 62 with EMPTY accessToken | 0 with refreshToken
.credentials.json.bak.mcp-fix   65 mcpOAuth entries | 63 with EMPTY accessToken |  2 with refreshToken
.credentials.json.bak-manual    65 mcpOAuth entries | 63 with EMPTY accessToken |  2 with refreshToken

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 accessToken and no refreshToken
. 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 single serverUrl:

…|59e4938976c99701   clientId 30369d14…   accessToken 50 chars   refreshToken present   expiresAt 1779738805103
…|e4adb5291339918d   clientId d6474d66…   accessToken EMPTY      no refreshToken        no expiresAt
…|5bd626a1ec0f6b10   clientId a7bb9b2a…   accessToken 50 chars   refreshToken present   expiresAt 1783370246423

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 throws
Cannot read properties of undefined (reading 'map') after registration succeeds. So every retry
is 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.json shows the needs-auth caching behaviour you describe, with a
single entry for the affected server and no indication that credentials were ever lost.

AboulEinein · 1 month ago

Caught this wipe in the act on macOS, and the trigger looks narrower than a concurrency race: the mcpOAuth section is emptied by the same write that rotates the claudeAiOauth account token.

Environment

  • Claude Code 2.1.241, installMethod: native (~/.local/bin/claude → ~/.local/share/claude/versions/2.1.241)
  • macOS 26.6, Darwin 25.6.0, arm64. Credential storage: Keychain item Claude Code-credentials
  • 4 remote HTTP MCP servers with OAuth (one self-hosted, plus the figma / linear / slack plugin servers)
  • Roughly a dozen concurrent claude CLI sessions, several alive for days

Method

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

2026-08-24T12:47:28Z  acct 3b74a3d6  exp 1787584440553      | serverA access 085b07c9  refresh true  exp 1787570615294
2026-08-24T12:53:29Z  acct 3b74a3d6  exp 1787584440553      | serverA access de4fbb3b  refresh true  exp 1787579602904
2026-08-24T15:16:26Z  acct 2eda68a1  exp 1787613382524.616  | mcpOAuth: {}
2026-08-24T15:58:46Z  acct 2eda68a1  exp 1787613382524.616  | serverA access ""  refresh false  clientId <new>

Line 2 is the healthy control: an ordinary MCP token refresh rotates that one entry's accessToken and expiresAt and touches nothing else.

Line 3 is the wipe. The account token fingerprint changes and mcpOAuth becomes {} in the same write. The previous claudeAiOauth.expiresAt was 1787584440553 = 2026-08-24T15:14:00Z, so this is the account token being refreshed at expiry.

Why this looks like more than last-writer-wins

  1. mcpOAuth is 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.
  2. expiresAt changes 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.
  3. The cadence matches the account token, not session activity. New expiry 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:35 then 08-23T15:12; 08-21T19:54 then 08-21T21:50).
  4. It happens with zero MCP activity. Across ~/Library/Caches/claude-cli-nodejs/*/mcp-logs-*/, there is not one log line between 2026-08-23T15:38:20Z and 2026-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:

2026-08-20T12:38:14   figma, linear      No token data found
2026-08-23T11:35:54   serverA, figma, linear, slack   No token data found
2026-08-24T08:00:24   serverA, figma, linear, slack   No token data found

Aftermath

By 15:58:46Z the entries are back as tokenless stubs: accessToken: "", no refreshToken, no expiresAt, with clientId / redirectUri / discoveryState present. Two servers carry brand-new clientIds 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 while claude mcp list still reports them healthy.

Suggested area to look at

The account-token refresh path appears to write a credentials object built from claudeAiOauth alone rather than mutating the stored blob, so mcpOAuth is 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.

rbrookec-cloud · 1 month ago

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-credentials service holding the account login updates in place correctly, and its mdat advances on the same days connector auth is lost, which is consistent with mcpOAuth being cleared by the write that rotates claudeAiOauth.

Differs from #59460 in shape: those orphans live inside one keychain item under mcpOAuth.<serverName>|<urlHash>; these are separate top-level services.

rbrookec-cloud · 1 month ago

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-level Claude Code-credentials-<hash> services, 1-3/day, each written once and never read — and the correlation with the account-token mdat advancing on the same days still holds, which is the part relevant to this issue. #92149 has been retitled and rewritten accordingly.