Windows/VSCode: concurrent sessions race on OAuth refresh against the file credential store (400 → forced re-login)

Status Open
Reported on v2.1.258
Maintainer reply None cached
Activity 7 comments · opened Sep 3, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

On Windows with the file-based credential store, concurrent Claude Code processes race when refreshing the shared OAuth token and force each other into a re-login.

This is the same class of bug as #24317 (closed as completed 2026-05-07, with several commenters reporting it still reproduces) and #76905 (closed as not planned by the inactivity bot). Both existing reports are macOS Keychain-specific. This report adds the Windows / plaintext .credentials.json / VSCode extension data point, which is a different code path: tengu_windows_credman is false for this account, so VG() resolves to the plaintext store RO and every process shares one file.

The mechanism, from the shipped extension bundle (extension.js, v2.1.258):

// refresh margin — identical in every process
var FD4 = 300000;                                  // 5 minutes
function mB$($) { return Date.now() + FD4 >= $; }  // "expiring soon?"

Every process independently decides to refresh inside the same 5-minute window before the 8-hour access token expires. pendingRefresh de-duplicates only within a single process, and there is no cross-process lock:

async refreshOAuthToken($) {
  if (!$.refreshToken) return null;
  if (this.pendingRefresh) return this.pendingRefresh;   // in-process only
  ...
}

Since refresh tokens rotate on use, the first process to POST wins and the rest present an already-consumed refresh token. Per RFC 9700, replaying a consumed refresh token is a reuse signal, and the whole token family gets invalidated — so a single lost race costs the user the 27-day refresh token, not just one request.

What Should Happen?

A refresh performed by one process should not log the others out. Options:

  1. Take a cross-process lock (lockfile next to .credentials.json) around the refresh, so only one process refreshes and the others wait and re-read.
  2. On a 400 from the refresh endpoint, re-read the credential file and retry once before surfacing a login prompt — the winning process has usually already written a valid token. (A disk-recovery path exists — tengu_oauth_401_recovered_from_disk — but it did not prevent the failure below.)
  3. Jitter the 5-minute refresh margin per process so N processes don't all fire in the same window.

Error Messages/Logs

From %APPDATA%\Code\logs\<ts>\window1\exthost\Anthropic.claude-code\Claude VSCode.log:

12:25:36.010 [info]    Getting authentication status          (x6 within 1ms)
12:25:36.010 [info]    OAuth tokens found in secure storage
12:25:36.563 [error]   Failed to refresh OAuth token: AxiosError: Request failed with status code 400
12:25:36.563 [warning] OAuth token refresh failed, using potentially expired token
12:25:36.609 [DEBUG]   [claudeai-mcp] Fetch failed (401) after 1 attempt(s)
12:25:38.411 [error]   Error fetching remote sessions: AxiosError: Request failed with status code 401

Credential state at the time (field lengths/dates only, no token material):

accessToken           108 chars, expires in 8h
refreshToken          108 chars, expires in 27 days
scopes                user:file_upload, user:inference, user:mcp_servers,
                      user:profile, user:sessions:claude_code

Steps to Reproduce

  1. On Windows, sign in to the Claude Code VSCode extension with a subscription account (file-based store; tengu_windows_credman off).
  2. Open several concurrent sessions in the extension — 5 claude.exe child processes of one extension host was enough here.
  3. Leave them running across the 8-hour access-token expiry.
  4. In the 5-minute window before expiry, the processes refresh simultaneously; losers get HTTP 400 and the session ends up prompting for /login.

Note on the setup-token workaround

claude setup-token is often suggested for this, but it is not a workaround for subscription users who use claude.ai connectors. From the binary's own strings:

Remote Control requires a full-scope login token. Long-lived tokens (from
`claude setup-token` or CLAUDE_CODE_OAUTH_TOKEN) are limited to inference-only
for security reasons.

...env-var and setup-token sessions default to user:inference only

Trading user:mcp_servers, user:sessions:claude_code, user:file_upload and user:profile away to stop being logged out is a steep price. #76905 made the same point before it was auto-closed.

CLAUDE_CODE_OAUTH_401_WAIT_MS, suggested in #24317, also does not apply here — the wait path is gated on an env-var token:

let T = Boolean(process.env.CLAUDE_CODE_OAUTH_TOKEN) || Boolean(wH());
if (T) { let y = npe(); if (y > 0) { /* wait for a rotated env token */ } }

Is this a regression?

Unknown — first investigated on the current version.

Claude Code Version

VSCode extension 2.1.258 (bundled native binary 2.1.258); npm @anthropic-ai/claude-code 2.1.259 also present on PATH.

Platform

VSCode extension (Windows native binary)

Operating System

Windows 11 Home 10.0.26200

Terminal/Shell

PowerShell 7 / VSCode integrated terminal

View original on GitHub ↗

3 Comments

Andrerg01 · 1 month ago

Same issue here.

shinya-kobo · 26 days ago

Adding a Windows / VS Code data point on 2.1.275. This looks like the same startup race, but in recent versions the loser seems to get a lock error instead of a forced re-login.

Environment: Windows 11, VS Code extension anthropic.claude-code 2.1.275 (win32-x64), file credential store (~/.claude/.credentials.json; the access-token expiry is exactly 8 h after each write).

Pattern: 3 times in 16 days (2026-09-03 09:41, 09-17 09:41, 09-19 10:25 JST). Always the first request of the morning in a session that was created the previous evening and left open overnight (on 09-19 the webview sent launch_claude with resume). The error shown:

Failed to refresh OAuth token: another Claude Code process is refreshing it or exited mid-refresh. This is usually transient; retry in a minute, and if it persists close other Claude Code processes or sign in again

The extension itself starts two Claude processes in the same instant on every startup (Claude VSCode.log):

10:25:36.478 [info] Spawning Claude with SDK query function - ... permission mode: default, version: 2.1.275 ...
10:25:36.486 [info] Spawning Claude with SDK query function - ... permission mode: auto, version: 2.1.275 ...
(same pair again at 10:27:17.206 / 10:27:17.217 after a second window restart)

If the access token expired overnight, both processes see it as expired at the same moment, which would make this race deterministic on the first launch of each day. (I can only infer the refresh contention; the extension log does not print the refresh attempts themselves.)

Timeline on 09-19: first error at 10:25:12 (23 s after the window opened at 10:24:49). A retry after I restarted the window failed again at 10:25:52. .credentials.json was rewritten at 10:25:54 (new expiry +8 h). So the refresh did complete about 42 s after the first failure, but I can't tell which process finished it because I restarted VS Code in between. One second after the spawn, the claude.ai connectors also failed (MCP server "claude.ai Gmail": Failed to fetch tools: Connection closed, same for Google Drive, Terminal connection error 1/3), which recovered after the token was refreshed.

No .oauth_refresh.lock directory was left behind afterwards, so this is the contended-lock case rather than the stale-lock leak described in #95236.

Suggestion, in the spirit of the options above: refresh once in the parent (extension host) before spawning the child processes, or make the process that loses the lock wait for the winner and re-read .credentials.json instead of surfacing the error to the user.

Clemens123 · 16 days ago

Additional data point: Windows + VS Code, still reproducing on 2.1.282 / 2.1.283, now with the cross-process lock in place

Environment: Windows 11 (build 26200), VS Code extension 2.1.282 (bundled native binary), npm @anthropic-ai/claude-code 2.1.283, subscription login, file credential store (~/.claude/.credentials.json).

Since this issue was filed, a cross-process lock (~/.claude/.oauth_refresh.lock) has been added. On Windows it does not seem to prevent the race. Instead it fails with EPERM under contention.

What happened (one morning, from the extension logs):

  1. VS Code started and restored its windows. About 10 Claude Code sessions started within ~20 s. The access token had expired overnight.
  2. Within ~5 s, all sessions tried to take the refresh lock. The log shows 63 of these errors across the processes:

``
06:33:38Z [ERROR] Error: EPERM: operation not permitted, mkdir 'C:\Users\<user>\.claude\.oauth_refresh.lock'
...
06:33:43Z [ERROR] Error: EPERM: operation not permitted, mkdir 'C:\Users\<user>\.claude\.oauth_refresh.lock'
``

  1. Right after that:

``
06:33:46Z [ERROR] OAuth refresh failed (expected): Request failed with status code 400
06:33:52Z [DEBUG] [servedCatalog] fetch (org) skipped: no-auth (OAuth refresh token is no longer valid; run /login to re-authenticate)
06:34:17Z [ERROR] API auth_error: OAuth refresh token is no longer valid; run /login to re-authenticate
``

  1. Every session needed /login. This happens every morning.

Observations:

  • EPERM looks like it is treated as a hard error, not as "lock is held, wait and retry". On Windows, mkdir can return EPERM instead of EEXIST while another process is creating or removing the same directory (delete-pending state). So under contention the lock is effectively bypassed.
  • The 400 is logged as "(expected)", as if losing the race were handled. But the replayed refresh token still seems to invalidate the whole token family, so every session loses the login.
  • No stale lock directory was left behind afterwards. This differs from #95236: the lock exists and gets cleaned up, but it fails exactly when many processes contend at once.

Suggested fix: treat EPERM (and EBUSY) from the lock mkdir on Windows like EEXIST, and retry with a backoff. A process that loses the lock should re-read .credentials.json instead of sending its own refresh.

Workaround that avoids the morning race for me: a Windows scheduled task (at logon, on unlock, and daily) that runs a single claude -p call when the stored expiresAt has passed. VS Code sessions then start with a valid token and do not all refresh at once. It does not cover the case where the token expires while several sessions are active.

Showing cached comments. Read the full discussion on GitHub ↗