Bash tool crashes with EEXIST mkdir on .claude directories mid-session (Windows) — root cause: ReadOnly attribute set by CLI

Resolved 💬 3 comments Opened Apr 19, 2026 by igormorais123 Closed Apr 23, 2026

Environment

  • OS: Windows 11 Pro 10.0.26200
  • Shell: Git Bash (C:\Program Files\Git\bin\bash.exe) invoked by Claude Code
  • Node: v22.22.2 (via fnm)
  • Claude Code: @anthropic-ai/claude-code (npm global), native binary claude-win32-x64
  • Path: %APPDATA%\fnm\node-versions\v22.22.2\installation\node_modules\@anthropic-ai\claude-code\bin\claude.exe

Summary

Mid-session, the Bash tool begins returning EEXIST: file already exists, mkdir '<path>' for every subsequent invocation. Happens not only on session-env/<sessionId> but also on plugins\data\* and (via Stop hook) on plugins\data\codex-openai-codex. All other tools (Read/Write/Edit/Glob/Grep) keep working. Only a full CLI restart clears it, and the bug recurs.

Root cause (confirmed on Windows)

claude.exe is setting the NTFS ReadOnly attribute on every directory it creates inside .claude\. Evidence — after a few weeks of use, 100% of the subdirectories under .claude\file-history\ carry the R flag:

> cmd /c 'cd /d C:\Users\IgorPC\.claude && attrib /s /d *' | findstr /b "R"
R  C:\Users\IgorPC\.claude\file-history\00dc43d9-...
R  C:\Users\IgorPC\.claude\file-history\01c8275b-...
R  C:\Users\IgorPC\.claude\file-history\02558627-...
... (hundreds more)
R  C:\Users\IgorPC\.claude\session-env\<sessionId>

These dirs are created by the CLI itself and inherit no R from parents (.claude\ has no R, user profile has no R, no desktop.ini, not under OneDrive sync path). So the flag is being actively set by the CLI, not applied by Windows/OneDrive/etc.

Node's fs.mkdirSync(path, { recursive: true }) on Windows (libuv) returns EEXIST when the path already exists as a directory and any component has FILE_ATTRIBUTE_READONLY, rather than treating it as a no-op the way POSIX does. Related upstream discussion: https://github.com/libuv/libuv/issues/2897 and https://github.com/nodejs/node/issues/43994 (similar class of bug).

So the failure mode is:

  1. CLI creates .claude\session-env\<sessionId> and sets ReadOnly on it.
  2. Later, the Bash tool calls (presumably) fs.mkdirSync(sessionEnvDir, { recursive: true }) as a safety net.
  3. libuv's Win32 mkdir sees the existing dir with R and returns ERROR_ALREADY_EXISTS → Node surfaces it as EEXIST.
  4. The call site does not catch EEXIST in the "already-a-dir" branch, so the tool call aborts.
  5. Every subsequent Bash call hits the same path → permanent failure until restart.

The Stop hook shows the same failure on .claude\plugins\data\codex-openai-codex, proving the issue is broader than session-env — any code path that re-mkdirs a previously-CLI-created dir on Windows is vulnerable.

Reproduction

Not deterministic per session but the gateway observed across 3 different sessions in 2 days:

  1. Normal Bash usage for ~30-60 minutes (dozens of calls).
  2. Parallel activity: 2+ background bash tasks via run_in_background: true, OR a long-running hook, OR a Stop hook firing on subagent cleanup.
  3. Subsequent Bash call fails with EEXIST on the session-env UUID.
  4. Every Bash call from that point on fails the same way. Other tools fine.

Setting run_in_background: false reduced but did not eliminate recurrences.

Expected behavior

Treat EEXIST as success when the existing path is a directory:

try {
  fs.mkdirSync(dir, { recursive: true });
} catch (e) {
  if (e.code !== 'EEXIST' || !fs.statSync(dir).isDirectory()) throw e;
}

And — ideally — stop setting ReadOnly on created directories. There is no documented reason a dir like session-env\<uuid> should be ReadOnly, and it's what tips Windows into the EEXIST edge case in the first place.

Workarounds users are applying

All are mitigations, not fixes. Documenting them in case they help prioritize:

  1. Boot launcher — PowerShell wrapper replacing claude.cmd, runs attrib -r $env:USERPROFILE\.claude /s /d + cleans stale session-env\* dirs before invoking the real binary. Fails mid-session (new dirs born with R).
  2. Hooks bash→Node port — rewriting project hooks from .sh to .js to avoid bash spawns entirely. Reduces pressure but doesn't fix root cause.
  3. run_in_background: false default — reduces concurrency of Bash calls. Partial mitigation.
  4. Background FileSystemWatcher — scheduled task running powershell -WindowStyle Hidden with [System.IO.FileSystemWatcher] on .claude\, stripping R within milliseconds of any new dir. This is the only mitigation that survives mid-session. Install as an onlogon scheduled task so it persists reboot. Source of truth for this workaround: the user's bin\claude-watcher.ps1.

None of these should be necessary. The fix is upstream.

Impact

  • Blocks all shell-based workflows mid-session.
  • Forces full restart + loss of in-flight tool context.
  • Stop hook also fails the same way, so the session termination is itself unclean.
  • On long GSD / pipeline work, loses hours.

Pointer

Happy to provide additional traces or run an instrumented build. The smoking gun is trivial to confirm on any Windows install: attrib /s /d %USERPROFILE%\.claude will show R on every CLI-created subdir after a few days of use.

View original on GitHub ↗

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