Worktree isolation silently disabled when lead CWD is stranded under .claude/worktrees/ (falls back to in-place edits)

Status Fixed / completed
Maintainer reply ✓ Yes — bcherny
Activity 4 comments · opened Jun 23, 2026 · closed Aug 17, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Summary

When a lead session spawns isolation: worktree subagents, isolation intermittently does not engage: the subagent edits the live working copy (and can land commits on the wrong branch / wrong clone) instead of getting its own worktree. A full session restart reliably restores correct isolation. Root cause is a CWD-based short-circuit in the EnterWorktree path, not the base-ref logic.

Environment

  • Claude Code v2.1.187 (macOS arm64)
  • Repo uses git linked worktrees: a main clone plus one or more git worktree add siblings sharing one object store
  • main clone: /repos/proj (the .git lives here)
  • working session runs in a linked worktree: /repos/proj.worktrees/wt1 (branch feature-branch)
  • Subagents spawned with isolation: worktree (both via agent frontmatter and Agent-tool calls), orchestrated by a lead session that fans out multiple subagents

Expected behavior

A spawn requesting isolation: worktree either gets a fresh isolated worktree, or hard-errors. It should never silently fall back to editing the lead's working copy.

Actual behavior

Intermittently, the subagent result carries no worktreePath/worktreeBranch trailer and its edits/commits land in place — on the lead's working copy, which may be the main clone parked on an unrelated branch with its own uncommitted WIP. Successful spawns in the same session do produce the worktree trailer, so it's intermittent, not total. Restarting the session fixes it.

Root cause (diagnosed from the v2.1.187 binary + a live probe)

1. Worktree directory location is fine and by design. The worktree dir is built as join(<root>, ".claude", "worktrees"), where <root> derives from the parent of git rev-parse --git-common-dir. For any linked worktree, --git-common-dir returns the shared main .git, so all agent worktrees live under the main clone's .claude/worktrees/. A controlled isolation: worktree probe spawned from the linked worktree confirmed this is harmless: the worktree landed under the main clone but was correctly branched off the linked worktree's HEAD (baseRef: "head") and was genuinely isolated. So this is not the bug.

2. The real bug is a CWD short-circuit in EnterWorktree. The guard (paraphrasing the in-binary string) is: "…unless your cwd is already under .claude/worktrees/, in which case the working copy is edited directly."

Failure chain:

  1. An isolated spawn leaves the lead's CWD inside the worktree it just ran in.
  2. That worktree is later removed / orphaned (normal cleanup, or a discarded task), but the lead's CWD path is still lexically under .claude/worktrees/.
  3. The next isolation: worktree spawn hits the guard → "cwd already under .claude/worktrees/" → isolation is skipped, and edits go to whatever the stranded CWD resolves to (main clone / wrong branch).
  4. Session restart resets CWD to the repo root → guard no longer fires → isolation works again.

Accumulated stale agent-* worktrees enlarge the surface for steps 2–3, and cleanupStaleAgentWorktrees won't clear worktrees held by another clone/session sharing the object store.

Reproduction

  1. In a repo with at least one git worktree add linked worktree, start a Claude Code session inside the linked worktree.
  2. Spawn an isolation: worktree subagent; let it run and return (lead CWD is now inside …/.claude/worktrees/agent-XXX).
  3. Remove/orphan that agent worktree (e.g. discard the task / git worktree remove) without the lead cd-ing back to the repo root.
  4. Spawn another isolation: worktree subagent.
  • Observed: second subagent edits in place; no worktreePath trailer.
  • Expected: a fresh isolated worktree, or a hard error.

Suggested fix

In the EnterWorktree path, when isolation is requested:

  • Detect when the current CWD is under .claude/worktrees/ but that worktree no longer exists / is orphaned, and re-resolve to the repo root and create a fresh worktree rather than treating "cwd under .claude/worktrees/" as "already isolated."
  • If isolation genuinely cannot be established, hard-error instead of falling back to in-place edits — silent fallback is the most damaging part (commits land on unrelated branches / another clone's WIP).

Impact

  • Subagent edits/commits land on the wrong branch or a sibling clone's working copy, including clobbering pre-existing uncommitted WIP in the main clone.
  • Silent: callers checking only "did I get a worktree?" still see plausible results until they inspect git state across clones.
  • Affects multi-worktree / multi-clone setups and any lead orchestrating multiple isolated subagents.

Workaround

  • cd back to the repo root after every isolated spawn; never let the lead's CWD sit under .claude/worktrees/.
  • Restart the session to reset CWD.
  • Prune stale agent-* worktrees regularly.

View original on GitHub ↗

3 Comments

kcarriedo · 3 months ago

The stranded-CWD path you traced is exactly right -- "cwd is already under .claude/worktrees/" as a proxy for "already isolated" is fragile the moment the worktree that CWD points to gets removed. A path-under-prefix check doesn't verify the underlying worktree still exists or belongs to the current session.

The failure consequence is the worst possible outcome from a "silent degradation" standpoint: the subagent believes it got isolation, callers checking for a worktreePath trailer may still see a plausible response, but the edits land on the lead's live working copy. A hard-error would at least fail fast and stop the blast radius at that one agent.

A few things worth noting from running this pattern at some scale:

  1. The fix you propose (re-resolve to repo root when CWD is under .claude/worktrees/ but that worktree no longer exists) handles the orphaned-worktree case cleanly. Worth also handling the case where the worktree path exists on disk but belongs to a different session's cleanup queue -- git worktree list --porcelain can confirm whether a given path is a registered worktree.
  1. The cd-back-to-root workaround is correct but easy to miss in a long orchestration run where the lead session accumulates many agent spawns. A defensive pattern that's helped: after every isolation: worktree spawn returns, the lead explicitly resolves git rev-parse --show-toplevel and uses that as the base for all subsequent relative paths, rather than relying on inherited CWD.
  1. The stale agent-* worktree accumulation compounds the exposure surface. If cleanupStaleAgentWorktrees can't remove worktrees held by another clone sharing the object store, there may be value in a read-only "report stale agent worktrees" command that the human can run periodically, even if auto-cleanup is unsafe.

(disclosure: building Claudeverse, a control plane for multi-agent Claude Code workflows -- this class of silent isolation failure is one of the core problems we're working around)

mikehenson · 3 months ago

Adding a deterministic trigger for this silent in-place fallback, still present on v2.1.201 (Linux/WSL2):

Resume-after-reap. A subagent spawned with isolation: worktree gets a correct isolated worktree. When it goes idle unchanged, that worktree is auto-reaped (removed). Resuming the same subagent (Agent-tool / SendMessage continuation) then runs it in the parent/main tree — git rev-parse --absolute-git-dir returns the plain repo .git instead of .git/worktrees/<name>. No worktree is re-established on resume, and edits/commits would land in place.

Deterministic repro in our environment:

  1. Dispatch a fresh isolation: worktree subagent → it reports absolute-git-dir = .../.git/worktrees/agent-XXX (correctly isolated). ✓
  2. Let it go idle with no committed changes → git worktree list shows the worktree auto-removed.
  3. Resume that same agent → it now reports absolute-git-dir = <repo>/.git (the main tree). ✗

Data points that may help triage:

  • Fresh dispatches isolate correctly for us on 2.1.201 — including when the lead session is itself inside a linked worktree (the subagent still gets its own separate worktree branched off the lead's HEAD via baseRef: "head"). So for us it is specifically the resume path that drops to in-place, which fits this issue's "CWD-based short-circuit → silent in-place fallback" diagnosis and the "restart fixes it" behavior (a fresh session has no stale/reaped-worktree CWD to short-circuit on).
  • Suggests the resume path doesn't re-run the EnterWorktree setup (or short-circuits on the now-removed worktree CWD) rather than re-establishing isolation.
bcherny collaborator · 1 month ago

Tried to reproduce this on 2.1.233 (Linux) using your exact steps: repo with git worktree add .claude/worktrees/wt1 -b wt1, lead started with cwd inside .claude/worktrees/wt1, then a subagent spawned with isolation: "worktree" to append to README.md and commit — plus a variant fanning out three concurrent isolated subagents from the same stranded lead.

In every run, isolation engaged: each subagent got a fresh worktree under the repo root's .claude/worktrees/, committed on its own worktree-agent-… branch, its result carried the worktree path/branch info, and the lead's checkout and branch were untouched.

We also dug into the history here. The "lead cwd under .claude/worktrees/ skips isolation" theory turned out to be incidental — nested-worktree working directories were already handled correctly, and there's a regression test covering that exact scenario. The actual bugs behind this symptom were (a) certain spawn shapes silently dropping the isolation request and (b) an intermittent bug, worst when fanning out several isolated subagents at once, where a subagent's shell commands could run in the parent checkout. Both were fixed in recent releases — see the 2.1.203 entry in the changelog ("Fixed worktree-isolated subagents sometimes running shell commands in the parent checkout…"), with further isolation hardening through 2.1.222.

Closing as fixed — if you still see a missing worktree trailer or in-place edits on 2.1.233 or later, reply here and we'll reopen.

🤖 Generated with Claude Code

Showing cached comments. Read the full discussion on GitHub ↗