[BUG] Subagent worktree mkdir is non-idempotent — recurring close-for-inactivity (refs #39680, #51023, #54706)

Resolved 💬 2 comments Opened Jun 1, 2026 by mclean Closed Jun 1, 2026

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 — --worktree flag panics with segfault when .claude/worktrees directory already exists — closed
  • #39680 — Agent tool with isolation: "worktree" fails with EEXIST when .claude/worktrees/ directory already exists — closed as NOT_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: Agent tool calls with isolation: "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 manually rm -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)

  1. Run any custom subagent / Agent tool call with isolation: "worktree" — succeeds.
  2. Wait for it to return (no need to clean up).
  3. 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.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗