Windows/VSCode: concurrent sessions race on OAuth refresh against the file credential store (400 → forced re-login)
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:
- 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. - 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.) - 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
- On Windows, sign in to the Claude Code VSCode extension with a subscription account (file-based store;
tengu_windows_credmanoff). - Open several concurrent sessions in the extension — 5
claude.exechild processes of one extension host was enough here. - Leave them running across the 8-hour access-token expiry.
- 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
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Same issue here.
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-code2.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_claudewithresume). The error shown:The extension itself starts two Claude processes in the same instant on every startup (
Claude VSCode.log):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.jsonwas 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.lockdirectory 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.jsoninstead of surfacing the error to the user.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-code2.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 withEPERMunder contention.What happened (one morning, from the extension logs):
``
``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'
``
``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
/login. This happens every morning.Observations:
EPERMlooks like it is treated as a hard error, not as "lock is held, wait and retry". On Windows,mkdircan returnEPERMinstead ofEEXISTwhile another process is creating or removing the same directory (delete-pending state). So under contention the lock is effectively bypassed.Suggested fix: treat
EPERM(andEBUSY) from the lockmkdiron Windows likeEEXIST, and retry with a backoff. A process that loses the lock should re-read.credentials.jsoninstead 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 -pcall when the storedexpiresAthas 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.