[BUG] Subagent worktree mkdir is non-idempotent — recurring close-for-inactivity (refs #39680, #51023, #54706)
Re-file
The bot at #54706 explicitly instructed: "Closing for now — inactive for too long. Please open a new issue if this is still relevant." This bug is still present and still blocking. Prior reports of the same root cause, all closed without a fix:
- #54706 — Subagent worktree: non-idempotent mkdir + process handle leak (Windows 11) — closed for inactivity
- #51023 —
--worktreeflag panics with segfault when.claude/worktreesdirectory already exists — closed - #39680 — Agent tool with
isolation: "worktree"fails with EEXIST when.claude/worktrees/directory already exists — closed asNOT_PLANNED - Same root-cause family: #33118, #36919, #37306, #39673
Environment
- Claude Code 2.1.72 (also reported in 2.1.85 at #39680 and 2.1.123 at #54706)
- Windows 11, Node v22.22.2
- Pattern:
Agenttool calls withisolation: "worktree", both parallel-in-one-message and serial-across-messages
What's wrong
The harness creates the worktrees parent directory with a non-recursive mkdirSync(path) (no { recursive: true }) at the start of every isolation: "worktree" dispatch. The first dispatch of a session creates .claude/worktrees/. Every subsequent dispatch in that session — even after the first sub-agent has fully returned — throws:
EEXIST: file already exists, mkdir 'C:\…\.claude\worktrees'
and the sub-agent never boots.
Minimal repro (Node, ~80 lines, no Claude Code dependency)
Demonstrates the bug surface and the proposed fix with the exact same fs API the harness uses. Self-contained, deterministic, ~50 ms.
// node repro.mjs
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
// BUG: non-recursive mkdir on a path that already exists -> EEXIST
const a = mkdtempSync(join(tmpdir(), 'cc-bug-'));
mkdirSync(join(a, '.claude'));
mkdirSync(join(a, '.claude', 'worktrees')); // dispatch 1: OK
try { mkdirSync(join(a, '.claude', 'worktrees')); } // dispatch 2: THROWS
catch (e) { console.log('bug:', e.code, e.message); }
rmSync(a, { recursive: true, force: true });
// FIX: recursive: true is idempotent
const b = mkdtempSync(join(tmpdir(), 'cc-fix-'));
mkdirSync(join(b, '.claude', 'worktrees'), { recursive: true }); // dispatch 1: OK
mkdirSync(join(b, '.claude', 'worktrees'), { recursive: true }); // dispatch 2: OK (no-op)
console.log('fix: idempotent ✓');
rmSync(b, { recursive: true, force: true });
Output on a current Node:
bug: EEXIST EEXIST: file already exists, mkdir 'C:\…\cc-bug-…\.claude\worktrees'
fix: idempotent ✓
Proposed patch
One line, no behavioral change beyond suppressing the spurious EEXIST:
- fs.mkdirSync(worktreesParentPath);
+ fs.mkdirSync(worktreesParentPath, { recursive: true });
Same fix the Node docs explicitly recommend for this use case. Same shape as #39673's stop-hook EEXIST regression.
Impact
- Blocks parallel multi-agent patterns whenever agents declare
isolation: "worktree"— second+ dispatch in a session always fails until the worktrees parent dir is manuallyrm -rf'd. - Combined with the auto-close-for-inactivity policy on the issue tracker, the bug has now silently survived at least three reporting cycles across a year.
- Operator-facing workaround we ship: a PreToolUse hook that reaps orphans +
rmdirs the empty parent before each Agent dispatch (cleanup-orphan-worktrees.mjs in our project). It works, but it's a workaround for a one-line fix.
Reproduction (against the actual harness)
- Run any custom subagent / Agent tool call with
isolation: "worktree"— succeeds. - Wait for it to return (no need to clean up).
- Run any second Agent tool call with
isolation: "worktree"— fails with the EEXIST above. Sub-agent never boots.
Ask
- Apply the one-line
{ recursive: true }patch at the harness's worktrees-parent mkdir site. - Optional but related: also fix the orphan worktree process-handle retention reported as Issue 2 in #54706 — that's a separate change, but the two together would close the substrate.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗