Worktree isolation (Agent/Workflow) forks from the default branch, not the current branch — wastes agent fan-outs on a divergent base
Summary
Spawning sub-agents with isolation: "worktree" (via the Agent tool or Workflow builder agents) creates the git worktree from the repository's default-branch lineage rather than the currently checked-out HEAD. When the working branch carries commits not yet on the default branch, every builder starts from a base that is missing the very work it was told to build on top of.
Severity: High — silent token waste, and a real risk of silently-wrong output.
Environment
- Working branch (e.g.
dev) is ahead of and divergent from the default branch (e.g.master) — it carries feature/migration commits not yet merged to the default. origin/HEADpoints at the default branch.
Steps to reproduce
- In a repo where the working branch is ahead of / divergent from the default branch, and
origin/HEAD→ the default branch. - Check out the working branch. Confirm
git rev-parse HEADis its tip. - Invoke a
Workflowwhose builder agents useisolation: "worktree"(or a singleAgentcall withisolation: "worktree"). - Inspect the created worktree's
HEAD.
Expected
The worktree is cut from the session's current HEAD (the working-branch tip).
Actual
The worktree HEAD is on the default-branch lineage. git merge-base --is-ancestor <current-HEAD> <worktree-HEAD> returns false in both directions — the worktree is on a divergent lineage, not merely behind. Recent commits on the working branch are absent from it.
Impact
- A 17-agent fan-out all detected the wrong base and aborted with zero output — a large batch of tokens spent for nothing.
- Builders that do not self-check their base would produce code against stale state — silently wrong, and hard to catch downstream.
- Observed recurring across multiple sessions, not a one-off.
Root-cause hypothesis
The worktree base appears to resolve from origin/HEAD / the configured default (main) branch instead of the session's current HEAD/branch. Please verify the base-selection logic uses git rev-parse HEAD (current checkout), not the default branch.
Secondary issue
Worktrees created during aborted/failed runs are not auto-cleaned when they contain untracked artifacts (e.g. a node_modules/ directory), so stale worktrees and their worktree-* branches accumulate across sessions and require manual git worktree prune + directory removal + branch deletion.
Suggested fixes
- Cut worktrees from the current
HEAD, notorigin/HEAD/ the default branch. - Have each spawned worktree assert its base is a descendant of the parent session's
HEAD, and fail loudly (actual-vs-expected SHA) if not — instead of silently proceeding. - Reliably clean up worktrees on run abort/failure regardless of untracked artifacts.
Workarounds found
- Run the fan-out in the main working tree with disjoint write-sets (no
isolation: "worktree") — a DAG that guarantees non-overlapping files makes isolation unnecessary. - Or
git remote set-head origin <working-branch>before spawning worktree-isolated agents (unconfirmed as a complete fix).
Showing cached comments. Read the full discussion on GitHub ↗
7 Comments
The silent version is the one that'll bite hardest — the 17-agent abort at least fails loudly. When a worktree gets cut from the default branch instead of your current HEAD, any agent that doesn't validate its own base just builds on the wrong lineage and emits plausible output that quietly diverges from where the feature actually is.
Two workarounds until the spawn base is fixed:
git merge-base --is-ancestor <intended-headHEAD(or comparegit rev-parse HEADto the sha the orchestrator intended) and abort early on mismatch — turns silent divergence back into a loud failure.Does it cut from default even when the create runs from inside the feature branch's working dir, or only when the shell is still on the default branch?
It cut from default even though the working dir was on the feature branch. My main checkout was on the feature branch (dev) the whole time — git rev-parse HEAD and git worktree list both showed the main tree at the feature-branch tip — and every worktree still came from the default-branch (master) lineage. git merge-base --is-ancestor <feature-tip> <worktree-HEAD> was false in both directions (divergent, not just behind). So being on the feature branch didn't prevent it; the shell's current branch didn't seem to factor in at all.
The determinant looked like origin/HEAD / the detected "main branch": git symbolic-ref refs/remotes/origin/HEAD was pointing at origin/master at the time.
One caveat, to be straight about it: I didn't re-test worktree isolation after git remote set-head origin <feature-branch> — I sidestepped by dropping isolation and running the fan-out in the main tree with disjoint write-sets. So I can confirm "shell on feature branch + origin/HEAD→default ⇒ worktree cut from default," but not yet that repointing origin/HEAD alone fully fixes it. A clean repro matrix would be {shell on default, shell on feature} × {origin/HEAD→default, origin/HEAD→feature}; my bet is origin/HEAD is the sole determinant and the shell branch is irrelevant, but that's a hypothesis.
Both workarounds are right, and the base-assert one is literally what saved this run: the agents I spawned carried a "first, assert your base is a descendant of the intended HEAD and abort on mismatch" step, so all 17 failed loudly instead of emitting plausible-but-wrong output. That early merge-base check is exactly what converts the silent-divergence case back into a loud one. Pinning the resolved HEAD sha into the worktree-create is the stronger fix (removes the inference entirely); the assert is the cheap backstop for when you don't control the create.
The silent version is what makes this one dangerous. An agent that errors out loudly because it is working from a stale base is bad. An agent that completes "successfully" but builds on a base that is missing your feature branch's changes is worse -- because the output looks right and you might not catch it until integration.
The fix at the application layer for now: run
git worktree add .claude/worktrees/<name> HEADmanually before invoking an agent with isolation: worktree, then point the agent at that path explicitly. That forces the worktree to cut from your current HEAD rather than the default. It is a workaround that should not be necessary.The longer-term ask -- worktree isolation should resolve from HEAD in the current working directory, not from the upstream default branch -- is correct. The current behavior likely comes from the worktree being created with a branch reference (
origin/HEAD) rather than the raw commit hash of the session's working directory. Switching togit worktree add <path> $(git rev-parse HEAD)in the isolation setup path would fix it without any API surface changes.If anyone has a reproduction setup with a clearly divergent feature branch they can share, a minimal failing case would make this much easier to get prioritized.
Your forensics line up with origin/HEAD being the determinant. That points to worktree isolation resolving the base from the detected "main branch," where the detection reads
git symbolic-ref refs/remotes/origin/HEADrather than the working-tree HEAD. If that's the path, the shell's current branch never factors in, because the create never looks at where you are, only at where the remote says "default" is. So your bet is well-founded. The {shell on default, shell on feature} axis should collapse, and origin/HEAD alone should decide. The one cell worth watching is whether a localgit remote set-head origin <feature>is read at create time or cached earlier in the session. That's the gap your caveat flags.The fix hierarchy you landed on is right. Pinning the resolved HEAD sha into the worktree-create removes the inference entirely, which is the upstream ask. The base-assert (
merge-base --is-ancestor <intended-HEAD> <worktree-HEAD>, abort on mismatch) is the portable backstop for when you don't control the create, and as you saw, it's what converts silent divergence back into a loud failure. All 17 aborting is the correct outcome. The failure you can't afford is the subset that don't self-validate and emit plausible-but-wrong output against the stale base.That "assert your base before you build" step generalizes past worktrees. It's the same cheap invariant that catches any fan-out where the workers inherit a base nobody re-checked. This silent-divergence-in-fan-out class is what I spend my time on, so if you run that matrix I'd be curious what the grid shows.
Ran the one cheap cell from my side. Since I'd repointed
origin/HEADto the feature branch mid-session (after it started cutting from master), I had a clean discriminator: spawn a worktree-isolated agent now and see whether it lands on feature-lineage (create-time read) or master-lineage (cached from session start).It cut from the feature branch — worktree HEAD was the current feature tip, and
origin/HEADinside the worktree resolved to the feature branch. So the base is read fromorigin/HEADat create time, not cached.Combined with the original failure (same shell branch, but
origin/HEAD→master at the time → worktree from master), that's a controlled 2-cell comparison holding the shell branch constant:origin/HEADis the determinant, read live, and the shell's current branch doesn't factor in — as you predicted. The {shell on default} axis is untested but should collapse for the same reason (the create never looks at where you are, only at where the remote says "default" is).Practical upshot:
git remote set-head origin <feature>is a complete workaround, since it's read at create time. The stronger fix is still pinning the resolved HEAD sha into the create so there's noorigin/HEADinference at all.That settles it. Holding the shell branch constant across the mid-session repoint is the clean discriminator, and origin/HEAD-read-at-create is exactly what makes
set-heada complete workaround rather than a partial one. Worth filing the pin-the-resolved-sha version as the actual ask, since it drops the inference entirely.set-headcovers the config side and the base-assert covers the runtime side in the meantime.That settles it. Holding the shell branch constant across the mid-session repoint is the clean discriminator, and origin/HEAD-read-at-create is exactly what makes
set-heada complete workaround rather than a partial one. Worth filing the pin-the-resolved-sha version as the actual ask, since it drops the inference entirely.set-headcovers the config side and the base-assert covers the runtime side in the meantime.