Worktree/cwd state defects for concurrent and nested agent sessions: sibling cwd races + idle auto-reap of a live child's worktree

Status Open
Maintainer reply None cached
Activity 0 comments · opened Aug 14, 2026

Summary

Two related CLI-level defects in how the Claude Code CLI manages worktree/cwd state for concurrent and nested agent sessions, both observed in production on kimgranlund/claude-plugins:

(a) Racy cwd/worktree state across sibling agent sessions launched from one background job. Sibling sessions intermittently read/write a DIFFERENT sibling's cwd or worktree — sometimes a loud rejection naming the wrong path, sometimes a silent cwd swap on a Bash tool call.

(b) Idle-triggered auto-reap of an unchanged worktree while a live nested child session still references it. The harness's worktree auto-cleanup evaluates only whether a worktree has uncommitted changes, not whether a live nested dispatch is still anchored to it — so a parent session that dispatches a nested child into its own worktree and then idles (waiting on that child, zero interim changes) gets its worktree deleted out from under the still-running child.

Environment

  • Claude Code CLI, macOS (Darwin 25.x)
  • Background jobs (/goal, scheduled/cloud routines) launching multiple agent sessions, each isolated via git worktree under a repo-local .claude/worktrees/<name>/ convention
  • Nested agent dispatch: a parent session (itself worktree-isolated) dispatches a child agent that inherits/references the parent's own worktree path rather than getting its own

Repro sketches

(a) Sibling cwd race:

  1. Launch N sibling agent sessions from one background job (e.g. a chore-lead-style fan-out), each assigned its own worktree path.
  2. Have each sibling run ordinary Edit/Bash/git operations against its assigned path, no shared files.
  3. Intermittently (confirmed across multiple runs, one production incident with 3 concurrent siblings), one sibling's EnterWorktree/ExitWorktree, or a plain Edit/Bash call, resolves against a DIFFERENT sibling's path instead of its own pinned one — either erroring naming the wrong path, or silently succeeding against the wrong tree.
  4. Separately observed in the same incident: the harness resets a session's shell cwd between Bash calls to that session's pinned directory — when the PIN itself has drifted (per point 3), this "reset" reintroduces the wrong path rather than correcting it.

(b) Idle worktree auto-reap racing a live nested child:

  1. A parent session (build-lead-shaped) is isolated into its own worktree, e.g. .claude/worktrees/agent-XXXX.
  2. It dispatches a nested child agent into that SAME worktree path (no separate worktree created for the child) and then goes idle, waiting on the child's completion — with zero uncommitted changes in the worktree at that moment (the child hasn't written anything yet).
  3. The harness's idle-worktree auto-cleanup reaps the worktree because its only eligibility check is "unchanged," with no notion of a live process/dependent still anchored to that path.
  4. The child's next Bash/Edit call fails: working directory no longer exists ... Refusing to run there (the CLI's own refusal guard fires correctly — no silent redirect into the shared primary checkout occurred, zero data loss) — but the child is stuck until a human/host manually recreates the worktree and resumes it.

Expected vs actual

| | Expected | Actual |
|---|---|---|
| (a) | Each sibling session's cwd/worktree pin is isolated and immune to cross-contamination from concurrent siblings of the same job | Pins can leak or swap across siblings; the periodic host-side cwd reset can reinforce a wrong pin instead of a correct one |
| (b) | A worktree with a live nested dependent (child session still referencing the path) is never eligible for idle auto-reap, regardless of dirtiness | Cleanliness (no uncommitted changes) alone qualifies a worktree for reaping, even while a live child is still running inside it |

Workarounds that hold (estate-side, cited by PR)

None of these fix the CLI-level root cause — they are mitigations and recovery procedures this repo (kimgranlund/claude-plugins, teamwork plugin) has shipped and validated:

  • (a) Sibling-cwd-race mitigation — strict writer serialization (one sibling writing at a time) plus a mandatory cd <path> && pwd && git status verification before every write. Documented as standing doctrine in teamwork/skills/parallel-work-rules/SKILL.md ("Standing mitigation: sibling-session cwd races").
  • (a) Partial mechanical guardworktree-prebash-guard (teamwork 2.9.4, PR #208) flags a Bash command that cd's from one worktree into either the primary checkout or a SIBLING worktree in the same compound call. It is ASK-only (never a hard block, by this repo's own hook-writing doctrine) and has disclosed blind spots: dynamic $(...)/` ... cd targets and sh -c/bash -c` wrapper strings are not resolvable without executing the shell, so they pass silently. The serialization + cd-pwd-verify discipline above is belt-and-suspenders on top of this guard, not made redundant by it.
  • (b) Validated recovery procedure — once a live child hits the "working directory no longer exists" refusal: the host recreates the worktree at the EXACT same path on the claimed branch (git worktree add <same-path> <claimed-branch> — the branch survives the reap since it was created at claim time; falls back to -b off main only if the branch itself is gone too), verifies the recreated worktree is clean and on the correct HEAD, then messages the child to resume with an explicit cd-per-Bash-call instruction (since the child's pinned cwd may still be stale even though the path exists again). Documented in the same SKILL.md ("Recovery: a live agent's worktree vanished mid-dispatch").
  • (b) Adjacent lifecycle fixes already shipped in this repo, related but distinct territory: worktree reuse now keys off identity rather than raw path (PR #200); a verified branch/worktree teardown gate proves clean retirement before a build seat retires (PR #201); nested dispatch in this repo's own dispatch-ticket procedure now always forces its OWN fresh worktree rather than inheriting the parent's, closing off the specific "child stuck without an anchor" shape from the estate side (teamwork 2.10.0, PR #212) — none of these touch the CLI's own reap-eligibility check or cross-sibling cwd isolation, which is why this issue exists.

Tracking

Originally tracked as two issues in kimgranlund/claude-plugins: #189 (sibling cwd race) and #207 (idle-reap vs. live nested dispatch), split 2026-08-13 into estate-side levers (both shipped) and this CLI-level remainder (neither issue closes on the estate work alone).
Originally tracked as two issues in kimgranlund/claude-plugins: #189 (sibling cwd race) and #207 (idle-reap vs. live nested dispatch), split 2026-08-13 into estate-side levers (both shipped) and this CLI-level remainder.

View original on GitHub ↗