session-env dir locks and requires manual rmdir to unstick Bash tool on Windows

Status Fixed / completed
Reported on v2.1.111
Maintainer reply None cached
Activity 12 comments · opened Apr 17, 2026 · closed Jun 4, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When the Bash tool runs, it intermittently fails with:

EEXIST: file already exists, mkdir 'C:\Users\User\.claude\session-env\<uuid>'

The UUID is fixed for the life of the Claude Code session. Once the first EEXIST happens, every subsequent Bash call in the session fails the same way. Non-Bash tools (Read, Edit, Grep) continue to work fine. The only way to recover is to close Claude's reach and manually rmdir the offending folder from an external cmd window, then the next Bash call succeeds — until the next lock, which typically happens 3–4 calls later.

It looks like the shell session-env is being torn down and re-provisioned on every Bash call (the "Shell cwd was reset to ..." line appears after each successful call), and the setup step mkdirs the session-env dir without tolerating EEXIST when the previous teardown didn't clean up.

What Should Happen?

The Bash tool should either:
(a) make the session-env setup idempotent (tolerate EEXIST / use mkdir -p semantics), or
(b) ensure the teardown reliably removes the dir so the next setup starts clean.

Either way, Bash calls should continue working without manual user intervention.

Error Messages/Logs

EEXIST: file already exists, mkdir 'C:\Users\User\.claude\session-env\1ff603cc-ffaa-47b5-9d71-b06ac4350e59'

Steps to Reproduce

  1. On Windows 11, start Claude Code from a cmd prompt: claude
  2. In the Claude session, issue several Bash tool calls in sequence.

Any work that involves running node --check, git status, git commit,
gcloud, etc. back-to-back will reproduce. E.g.:

  • ask Claude to run node --check on one or two files
  • ask Claude to commit the changes
  • continue editing and committing for a few iterations
  1. Within ~3–10 Bash calls, one will fail with:

EEXIST: file already exists, mkdir 'C:\Users\User\.claude\session-env\<uuid>'

  1. Every subsequent Bash call in the same session fails identically.
  2. Recover by running in a separate cmd window:

rmdir /s /q "C:\Users\User\.claude\session-env\<uuid>"

  1. Next Bash call works. The cycle repeats.

In a ~2-hour engineering session with periodic commits/checks, the
failure surfaced ~6 times and required ~6 manual rmdirs.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

Unknown

Claude Code Version

2.1.111

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

Windows Terminal

Additional Information

The session-env UUID is stable across the entire Claude session — it's
not regenerated per tool call. The failure also seems to persist across
"Shell cwd was reset to ..." messages, suggesting those resets don't
actually teardown the session-env dir; they just reset cwd to default.

Workaround users can apply today, if they hit this:
rmdir /s /q "C:\Users\User\.claude\session-env\<uuid>"
(where <uuid> is whatever appears in the error).

Only observed on Windows. No repro on macOS/Linux in my usage.

Impact: blocks shell-dependent tool use mid-session. In practice this
kills long autonomous engineering flows because the human has to
manually intervene every few tool calls — the model can't unstick
itself, since Bash is exactly what's broken.

View original on GitHub ↗

11 Comments

0xbrainkid · 4 months ago

The EEXIST on a fixed-per-session UUID points to the directory creation code running on every Bash tool invocation rather than once at session initialization — without an existence check or { recursive: true } guard.

Likely sequence: session starts, UUID is generated, session-env dir is created successfully on the first Bash call. A subsequent Bash call in the same session tries to create the same path again (perhaps initializing on each invocation), hits EEXIST, and throws instead of continuing.

Fix: Move session-env/<uuid> directory creation to session startup (once), using fs.mkdirSync(path, { recursive: true }). If it is intentionally lazy (created on first Bash use), add an existence check: if (!fs.existsSync(path)) fs.mkdirSync(path).

Stale dir case: If EEXIST comes from a prior crashed session that left the directory behind, add a startup sweep that removes session-env/ entries older than the current process start time.

Workaround: rmdir /s /q %APPDATA%\.claude\session-env\<uuid> after each crashed session to clear the stale lock. Tedious but recoverable until fixed.

rappstar · 4 months ago

I get this even when sessions don't crash. My agent was hitting this almost every time it ran bash to run python.

ThePaulius · 4 months ago

Using PowerShell as a workaround, this has been going on for some time now :(

chadmyers · 4 months ago

+1 on Windows 11 Pro (26200), Git Bash routed through the Bash tool. A few data points that may narrow this down:

The session-env dirs are empty. I inspected the stuck UUID and several older ones; recursive child count was 0 for all of them. The dir appears to be a marker/sentinel, not per-call state, which makes a fix cheaper: an existsSync guard or { recursive: true } is enough; no content to preserve.

They never get cleaned up. I had 32 dirs going back 10 days before manually clearing. Cleanly-exited sessions also seem to leak.

Wholesale clearing is safe even with a Claude session live. rmdir /s /q "%USERPROFILE%\.claude\session-env\*" against all entries (not just the active UUID) does not break the running session; the next Bash call recreates the active UUID dir cleanly. Useful workaround for users who don't know which UUID is active or who have multiple Claude surfaces running.

Same-UUID re-failure within one session. Bash worked for ~7 calls, broke on UUID X. External rmdir of everything; bash worked another ~4 calls, then broke again on the same UUID X. The harness commits to one UUID per session and keeps retrying that exact path even after the dir has been externally deleted and recreated. A startup-time existence check alone won't fix it; the per-call mkdir itself needs to be idempotent.

Net: matches 0xbrainkid's analysis. Cheapest fix is making the per-Bash-call mkdir tolerate EEXIST (fs.mkdirSync(p, { recursive: true })). Startup sweep of stale dirs is nice-to-have but not blocking; users can ignore the buildup.

rappstar · 4 months ago

To what Chad said, you can delete these mid-session. So what I have as a temporary - but effective - fix is a pre-call hook to just wipe the directory before every bash call. I haven't bricked since. in case this helps anyone else:

"hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "powershell -NoProfile -Command '$p = $env:USERPROFILE + \"\\.claude\\session-env\"; if (Test-Path $p) { Get-ChildItem $p -Directory | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue }'"
          }
        ]
      }
    ]
  }
donizetiferr · 3 months ago

Confirming this is still happening on v2.1.126 (Windows 11 Home build 26200, Node v22.22.2, PowerShell 5.1.26100). Long sessions are particularly affected.

New evidence: bug is not Bash-specific

Cross-referenced 21 EEXIST occurrences in a single 5-day session log (.claude/projects/<project>/<session>.jsonl) by mapping each error's tool_use_id back to the parent tool_use block. Distribution:

| Tool that triggered the failed mkdir | Count |
|---|---|
| Bash | 6 |
| PowerShell | 2 |
| Agent (subagent spawn) | 1 |
| Write | 1 |
| Other / undetermined | 11 |

The error path runs through whatever shared "shell-like execution context" provisioning step the harness invokes — flipping CLAUDE_CODE_USE_POWERSHELL_TOOL=1 to prefer PowerShell does not route around it. Subagent spawn and even Write calls have hit the same EEXIST on session-env/<UUID> mkdir, suggesting the mkdir lives above the per-tool layer.

Orphan accumulation pattern

Same UUID reuse pattern as #54689: a fresh install of v2.1.126 accumulated 149 orphan session-env/<UUID>/ directories over 18 days (16/04 → 04/05), all empty (the actual shell state had been freed; only the placeholder dirs leaked). The vast majority correlate to sessions terminated by timeout, force-kill, or crash — none of which run the Stop hook, so any cleanup hook there can't catch them.

Local 3-layer workaround (pasted in case anyone else wants it)

  1. Cleanup script at ~/.claude/hooks/cleanup_session_env.ps1 deleting any session-env/<UUID>/ not modified in the last 2h:

``powershell
$ErrorActionPreference = 'SilentlyContinue'
$base = Join-Path $env:USERPROFILE '.claude\session-env'
if (-not (Test-Path $base)) { return }
$cutoff = (Get-Date).AddHours(-2)
Get-ChildItem $base -Directory | Where-Object { $_.LastWriteTime -lt $cutoff } |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
``

  1. Stop hook entry in ~/.claude/settings.json invokes the script on clean session termination.
  2. Windows Scheduled Task runs the same script daily at 04:00 (covers crashes/timeouts that don't fire Stop):

``powershell
$action = New-ScheduledTaskAction -Execute "powershell.exe"

-Argument "-NoProfile -ExecutionPolicy Bypass -File "$env:USERPROFILE\.claude\hooks\cleanup_session_env.ps1""
Register-ScheduledTask -TaskName "ClaudeCode-CleanupSessionEnv"
-Action $action -Trigger (New-ScheduledTaskTrigger -Daily -At 04:00)

-Settings (New-ScheduledTaskSettingsSet -StartWhenAvailable)
-Principal (New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive)
``

This keeps the orphan count near zero, but does not fix the in-session lock — once EEXIST fires mid-session, the only reliable recovery is still to exit and claude --continue.

Suggested upstream fix

fs.mkdir(sessionEnvPath, { recursive: true }) (or try { mkdirSync(...) } catch (e) { if (e.code !== 'EEXIST') throw e }) at the provisioning callsite would resolve the symptom for every affected tool path. Whether the deeper fix is to also stop reusing UUIDs across sessions (#54689) is a separate question, but idempotent mkdir is the cheap immediate fix that unblocks every report linked to this thread.

MikeHigman · 3 months ago

The Bash tool intermittently fails partway through a session with:

EEXIST: file already exists, mkdir 'C:\Users\mikeh\.claude\session-env\<session-id>'
Once it hits, every subsequent Bash call in that session fails with the same error. The session has to be abandoned or worked around (Read/Write/PowerShell tool instead of Bash). It's happened in 6+ sessions over the past 2 weeks across unrelated workflows.

Expected: Bash either reuses the existing session-env directory or recreates it cleanly.

Actual: Permanent Bash failure for the rest of the session.

Suspected cause: Race between Claude Code's existence check and mkdir — likely Windows Defender (or similar AV) scanning the newly created directory in between. Adding the path to Defender exclusions would be a workaround, but the tool should handle EEXIST defensively (treat as success if the dir exists and is writable).

Repro: Not deterministic, but happens within a few hours of normal use on Windows. The session-env folder accumulates one subdirectory per session (44 in mine, dating back ~2 weeks), all 0 bytes — suggesting nothing else uses them after creation, so the mkdir guard is the only failure point.

Suggested fix: Wrap the mkdir with { recursive: true } (Node) or catch EEXIST and verify the directory is writable rather than failing.

Mcclellange · 3 months ago

Root cause confirmed: Bun's Windows mkdir(recursive: true) bug

Adding a diagnostic dump from binary analysis of Claude Code 2.1.128 on Windows that may be useful for the fix.

The mkdir IS already using recursive: true. I extracted the offending function from claude.exe via findstr //C:"session-env":

async function n9q() {
  let H = tf8.join(c6(), "session-env", y8());      // ~/.claude/session-env/<sid>
  return await xa.mkdir(H, { recursive: !0 }), H    // recursive:true
}

So fix (a) "tolerate EEXIST / use mkdir -p semantics" is already there in source — but it doesn't work because claude.exe is Bun-compiled, not Node. Confirmed by string markers in the binary:

bun v${t.bun_version}
_bun/client/  _bun/asset/  _bun/src/*  _bun/hmr  /bun:info

Size and PE32+ format also match bun build --compile output (~255 MB).

Bun has a long-standing Windows-specific bug where fs.mkdirSync(path, {recursive: true}) intermittently throws EEXIST when path already exists, particularly under concurrent invocation. Standard Node does NOT have this bug — verified locally:

// Node 22 — succeeds 200/200 on existing dir
for (let i = 0; i < 200; i++) {
  fs.mkdirSync('C:/Users/x/.claude/session-env/<existing-sid>', { recursive: true });
}
// success=200 fail=0

n9q() is invoked from multiple internal code paths — qnK (clear hook files), $nK (read session env), HnK (get hook file path) — and these can be called from settings reload / hook re-evaluation, not just from Bash tool teardown. That's why the lockup also reproduces under compact events and why a PreToolUse-only workaround misses cases.

Why user-level hook workarounds fail

The hook env doesn't include CLAUDE_SESSION_ID. Verified in a Bash tool call:

CLAUDECODE=1
CLAUDE_CODE_EXECPATH=C:\...\claude-code\2.1.128\claude.exe
CLAUDE_CODE_ENTRYPOINT=claude-desktop
AI_AGENT=claude-code_2-1-128_agent
# No CLAUDE_SESSION_ID

So community hook snippets that pre-clean session-env\$env:CLAUDE_SESSION_ID are silently no-op'ing. A working defensive hook has to clear all subdirs under session-env/ (or be smarter about identifying the current session). Setting CLAUDE_SESSION_ID (or a similar var) in the hook environment would help anyone running this kind of pre-flight.

Suggested fixes (in order of effort)

  1. Wrap the mkdir in try/catch that swallows EEXIST. Cheap, fully sidesteps the Bun bug:

``js
try { await xa.mkdir(H, { recursive: true }); } catch (e) { if (e.code !== 'EEXIST') throw e; }
``

  1. Stat-then-mkdir on Windows specifically (skip mkdir if the path is already a directory).
  2. Set CLAUDE_SESSION_ID in the hook env so user-level pre-flight workarounds can target the correct dir while #1 ships.
  3. (Upstream) report to Bun / track Bun's Windows mkdir-recursive issues.

#1 alone would fix it for everyone affected.

Repro context

  • Windows 11 (NTFS, native, no OneDrive sync on .claude)
  • Claude Code 2.1.128 (Bun-compiled)
  • No antivirus / indexer interference (verified — .claude not in any monitored path)
  • Frequency scales with session length and tool-call density; high-context models exacerbate the symptom (more wall-clock time per turn = more cycles for the timing-dependent Bun bug to land), but the bug is model-independent.
ThePaulius · 2 months ago

Closed as completed? It still does it every day.

chadmyers · 2 months ago

@ThePaulius The fix for me was related to either Microsoft OneDrive or Google Drive syncing the folder Claude is trying to work in. It marks files/folders "+R" (read-only) recursive which messes with the way Claude Code uses mkdir. The solution is to stop syncing those folders and then restart OneDrive/Drive (or reboot) and then do a recursive "attrib -R" on the folder to remove the read only attribute

ThePaulius · 2 months ago

I'll check that out! Thanks It's bound to be OneDrive! It always is!

Showing cached comments. Read the full discussion on GitHub ↗