Windows: ~/.claude.json corruption from concurrent sessions (race condition in config writes)
Description
On Windows 11, running multiple concurrent Claude Code CLI sessions causes repeated corruption of ~/.claude.json, resulting in:
JSON Parse error: Unexpected EOFon every session start- Loss of
oauthAccountdata, forcing re-login every session - Other active sessions breaking/displaying errors
- Hundreds of
.corruptedbackup files accumulating in~/.claude/backups/
Environment
- OS: Windows 11 Pro 10.0.26100
- Claude Code: v2.1.59 (native install, SEA binary)
- Shell: PowerShell 7
- Sessions: 3-4 concurrent CLI sessions (
claude --dangerously-skip-permissions)
Root Cause Analysis
I decompiled the bundled JS from the binary and traced the issue to the config write path:
1. Atomic write fallback (Pe function / writeFileAtomic)
The atomic write correctly uses temp file + renameSync. However, on Windows, renameSync fails with EPERM when another process has the target file open for reading. When this happens, the code falls back to a direct non-atomic writeFileSync:
// Pseudocode from decompiled source
function writeFileAtomic(path, data) {
let tempFile = `${path}.tmp.${process.pid}.${Date.now()}`;
try {
fs.writeFileSync(tempFile, data, { flush: true });
fs.renameSync(tempFile, path); // FAILS on Windows with EPERM
} catch {
// Fallback: non-atomic direct write - THIS IS THE BUG
fs.writeFileSync(path, data, { flush: true });
}
}
2. Lock contention (A0I function / saveConfigWithLock)
The lock uses proper-lockfile with lockSync(). When lock acquisition fails (stale lockfile from killed process, contention timeout), saveGlobalConfig catches the error and falls back to saveWithoutLock — a completely unlocked, non-atomic write:
function saveGlobalConfig(callback) {
try {
saveWithLock(configPath, defaults, callback);
} catch {
// FALLBACK: unlocked write when lock fails!
saveWithoutLock(configPath, mergedConfig, defaults);
}
}
3. The cascade
- Session A reads config → Session B reads config
- Session A acquires lock, writes (rename fails on Windows) → falls back to direct write
- Session B acquires lock (or fails), starts writing simultaneously
- One write truncates the other → corrupted JSON
- Session C starts, reads corrupted file →
Unexpected EOF - Corruption handler backs up the corrupted file, writes a minimal config (losing
oauthAccount) - All sessions now need re-login → triggers more writes → more corruption
Evidence
- 358 corrupted backup files accumulated in
~/.claude/backups/over ~1 week - The corrupted files are truncated JSON (50-90KB files reduced to fragments)
- Some corrupted files are as small as 50-157 bytes (just
{"clientDataCache":{"data":{},...}}) - Multiple corrupted files with timestamps within the same second (concurrent writes)
- The
oauthAccountfield is consistently lost, while transient fields liketoolUsageandclientDataCachesurvive (they get re-written most frequently)
Suggested Fix
- Don't fall back to non-atomic writes on Windows. Instead, retry the rename with a small delay, or use
MoveFileExwithMOVEFILE_REPLACE_EXISTINGvia N-API/native addon.
- Use Windows-native file locking (
LockFileEx/fs.openwith exclusive flags) instead ofproper-lockfilewhich uses directory-based locking that is unreliable on NTFS under high contention.
- Read-modify-write with CAS: Before writing, verify the file hasn't changed since it was read (the
tengu_config_stale_writetelemetry already detects this — make it abort the write instead of just logging).
- Separate volatile and stable config: Fields like
toolUsage,clientDataCache,cachedGrowthBookFeatureschange frequently but are non-critical. Storing them in a separate file would dramatically reduce write contention on the main config that holds auth data.
Workaround
Created a PowerShell file watcher (guard.ps1) that monitors ~/.claude.json, detects corruption via JSON parse validation, and auto-restores from an in-memory snapshot within ~200ms. Also merges back oauthAccount when it gets stripped by a concurrent write. Started via PowerShell profile as a background job.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗