[BUG] EEXIST on session-env mkdir despite recursive:true (v2.1.108, Windows)
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?
The Bash tool fails with EEXIST: file already exists, mkdir '<config-dir>\session-env\<session-id>' at random points in long-running sessions and reliably on session resume. The harness's mkdir call (in function CU1 of cli.js v2.1.108) uses {recursive: true} but is not wrapped in the try/catch-EEXIST pattern that other mkdir call sites in the same file use. Whenever the harness tries to create a session-env/<session-id> folder that already exists on disk (session-id reused across resumes, folder not cleaned from a previous run, or a sync filter/race left it behind), the Bash tool fails before the user's command runs.
In our case the folder accumulated 222 orphan empty session-env/<uuid>/ directories in 2 days of normal use (vs 209 in 14 days before we relocated config to a OneDrive-synced path on the same disk), so the leak happens regardless of sync — OneDrive only amplifies the timing.
What Should Happen?
Bash tool should not fail with EEXIST. Either mkdir with recursive: true should be idempotent (the documented Node.js behavior), or the call in CU1 should be wrapped in the same defensive try/catch-EEXIST pattern that cli.js already uses at other mkdir call sites.
Error Messages/Logs
EEXIST: file already exists, mkdir 'D:\.claude\session-env\12f6195a-0e63-4ad2-ae5c-65b5ffdbc94b'
Steps to Reproduce
- Run a Claude Code session that executes Bash tool calls (this creates ~/.claude/session-env/<session-id>/).
- End the session without letting cleanup run (close terminal, OS sleep, kill -9, or simply let folders accumulate across many sessions).
- Resume a session whose session-id matches an existing folder, OR open a new session in a config dir where many old session-env/* folders remain.
- Next Bash tool invocation → "EEXIST: file already exists, mkdir ..." before the command runs.
Reproduces reliably on Windows. We hit it daily for ~2 days before tracing it. Inspecting the minified cli.js confirmed the missing try/catch in CU1.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.108 (Claude Code)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Windows Terminal
Additional Information
**Root cause from cli.js (v2.1.108) inspection:**
```js
// Function CU1 — creates the session-env directory
async function CU1(){
let q = xR8(A7(), "session-env", L8()); // path.join(configDir, "session-env", sessionId)
return await aqz(q, {recursive:!0}), q
}
Compare to the defensive pattern used elsewhere in cli.js for mkdir:
){try{await jD5(q,{recursive:!0,...K})}catch(_){if(g1(_)!=="EEXIST")throw _}}
CU1 is missing the try/catch. Per Node.js docs, fs.mkdir(path, {recursive: true}) should not throw EEXIST when the path exists, but on Windows (likely interaction with reparse points, sync filters, or atime races) it does. Fix appears to be a one-liner: wrap the aqz call in CU1 (and any analogous helpers) in the same try/catch pattern.
Workaround we deployed (reliable):
SessionStart + PreToolUse(Bash) hook in ~/.claude/settings.json that deletes empty session-env/* entries older than 5 minutes before the harness gets a chance to call mkdir on a colliding path:
$cutoff = (Get-Date).AddMinutes(-5)
Get-ChildItem "$env:CLAUDE_CONFIG_DIR\session-env" -Directory -Force | ForEach-Object {
if ($_.LastWriteTime -lt $cutoff) {
$hasContent = Get-ChildItem $_.FullName -Force -Recurse -EA SilentlyContinue | Select-Object -First 1
if (-not $hasContent) { Remove-Item $_.FullName -Recurse -Force -EA SilentlyContinue }
}
}
This works but is invisible-friction Windows users shouldn't need. Happy to provide additional logs, repro project, or PR if helpful.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗