Windows: ~/.claude.json corruption from concurrent sessions (race condition in config writes)

Resolved 💬 3 comments Opened Feb 26, 2026 by viftode4 Closed Feb 26, 2026

Description

On Windows 11, running multiple concurrent Claude Code CLI sessions causes repeated corruption of ~/.claude.json, resulting in:

  • JSON Parse error: Unexpected EOF on every session start
  • Loss of oauthAccount data, forcing re-login every session
  • Other active sessions breaking/displaying errors
  • Hundreds of .corrupted backup 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

  1. Session A reads config → Session B reads config
  2. Session A acquires lock, writes (rename fails on Windows) → falls back to direct write
  3. Session B acquires lock (or fails), starts writing simultaneously
  4. One write truncates the other → corrupted JSON
  5. Session C starts, reads corrupted file → Unexpected EOF
  6. Corruption handler backs up the corrupted file, writes a minimal config (losing oauthAccount)
  7. 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 oauthAccount field is consistently lost, while transient fields like toolUsage and clientDataCache survive (they get re-written most frequently)

Suggested Fix

  1. Don't fall back to non-atomic writes on Windows. Instead, retry the rename with a small delay, or use MoveFileEx with MOVEFILE_REPLACE_EXISTING via N-API/native addon.
  1. Use Windows-native file locking (LockFileEx / fs.open with exclusive flags) instead of proper-lockfile which uses directory-based locking that is unreliable on NTFS under high contention.
  1. Read-modify-write with CAS: Before writing, verify the file hasn't changed since it was read (the tengu_config_stale_write telemetry already detects this — make it abort the write instead of just logging).
  1. Separate volatile and stable config: Fields like toolUsage, clientDataCache, cachedGrowthBookFeatures change 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.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗