claude-code - 💡(How to fix) Fix Bash tool crashes with EEXIST mkdir on .claude directories mid-session (Windows) — root cause: ReadOnly attribute set by CLI [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#50773Fetched 2026-04-20 12:13:25
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Timeline (top)
labeled ×3commented ×1cross-referenced ×1

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

Root cause (confirmed on Windows)

Fix Action

Fix / Workaround

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.

Code Example

> 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>

---

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

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.

extent analysis

TL;DR

The most likely fix is to modify the claude.exe code to stop setting the ReadOnly attribute on created directories and to treat EEXIST as success when the existing path is a directory.

Guidance

  1. Verify the issue: Run attrib /s /d %USERPROFILE%\.claude to confirm that the ReadOnly attribute is being set on created directories.
  2. Code change: Modify the claude.exe code to use a try-catch block when creating directories, similar to the example provided in the issue: try { fs.mkdirSync(dir, { recursive: true }); } catch (e) { if (e.code !== 'EEXIST' || !fs.statSync(dir).isDirectory()) throw e; }.
  3. Remove ReadOnly attribute: Update the claude.exe code to not set the ReadOnly attribute on created directories.
  4. Test the fix: Verify that the issue is resolved by running the modified claude.exe and checking that the ReadOnly attribute is not being set on created directories.

Example

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

Notes

The provided workarounds may help mitigate the issue, but they do not address the root cause. The fix should be applied upstream in the claude.exe code.

Recommendation

Apply the code change to stop setting the ReadOnly attribute on created directories and to treat EEXIST as success when the existing path is a directory, as this addresses the root cause of the issue.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

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.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix Bash tool crashes with EEXIST mkdir on .claude directories mid-session (Windows) — root cause: ReadOnly attribute set by CLI [1 comments, 2 participants]