Bash tool crashes with EEXIST mkdir on .claude directories mid-session (Windows) — root cause: ReadOnly attribute set by CLI
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 binaryclaude-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:
- CLI creates
.claude\session-env\<sessionId>and sets ReadOnly on it. - Later, the Bash tool calls (presumably)
fs.mkdirSync(sessionEnvDir, { recursive: true })as a safety net. - libuv's Win32 mkdir sees the existing dir with
Rand returnsERROR_ALREADY_EXISTS→ Node surfaces it asEEXIST. - The call site does not catch
EEXISTin the "already-a-dir" branch, so the tool call aborts. - 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:
- Normal Bash usage for ~30-60 minutes (dozens of calls).
- Parallel activity: 2+ background bash tasks via
run_in_background: true, OR a long-running hook, OR a Stop hook firing on subagent cleanup. - Subsequent Bash call fails with
EEXISTon the session-env UUID. - 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:
- Boot launcher — PowerShell wrapper replacing
claude.cmd, runsattrib -r $env:USERPROFILE\.claude /s /d+ cleans stalesession-env\*dirs before invoking the real binary. Fails mid-session (new dirs born withR). - Hooks bash→Node port — rewriting project hooks from
.shto.jsto avoid bash spawns entirely. Reduces pressure but doesn't fix root cause. run_in_background: falsedefault — reduces concurrency of Bash calls. Partial mitigation.- Background FileSystemWatcher — scheduled task running
powershell -WindowStyle Hiddenwith[System.IO.FileSystemWatcher]on.claude\, strippingRwithin 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'sbin\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.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗