Worktree/cwd state defects for concurrent and nested agent sessions: sibling cwd races + idle auto-reap of a live child's worktree
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 viagit worktreeunder 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:
- Launch N sibling agent sessions from one background job (e.g. a
chore-lead-style fan-out), each assigned its own worktree path. - Have each sibling run ordinary Edit/Bash/git operations against its assigned path, no shared files.
- 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.
- 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:
- A parent session (build-lead-shaped) is isolated into its own worktree, e.g.
.claude/worktrees/agent-XXXX. - 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).
- 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.
- 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 statusverification before every write. Documented as standing doctrine inteamwork/skills/parallel-work-rules/SKILL.md("Standing mitigation: sibling-session cwd races"). - (a) Partial mechanical guard —
worktree-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 andsh -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-boffmainonly 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-ticketprocedure 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.