* fix(#4881): trust worktree.baseRef:"head" in harness mode and keep the #4868 observation as what restores it under a WorktreeCreate hook The pre-dispatch base check still derived its harness-mode verdict from the retired #48 premise that the harness never reads worktree.baseRef: with "head" set and HEAD diverged from origin/HEAD it degraded every wave with baseref-head-ignored-by-harness. #4868 inserted an observation of a clean prior harness worktree at HEAD ahead of that comparison, but an execute-phase run never has one at the moment it checks — the base-check runs before any dispatch, a degraded wave creates no worktrees, and a wave that did run in worktrees has them removed and HEAD moved before the next check — so the common case was unchanged (#4881 repro states 1 and 4). Re-scope of the closed #4752 onto current next, with #4868 kept: - branch a trusts "head" in both isolation modes, the way the harness is measured to behave (#4588: three settings layers, three OSes), and the spawn-time exit-42 guard stays the observation-based backstop; - a Claude Code WorktreeCreate hook in any settings file the check reads, or a file that does not parse, withholds that trust — the hook creates the worktree without applying the setting — and the inferred comparison runs, degrading with baseref-head-bypassed-by-hook; - the #4868 observation (b2) now sits behind branch a: it is reached only when "head" was not trusted outright, and on a hook host it is what restores the trust — a hook that forks from HEAD leaves exactly that evidence, one that forks elsewhere never does. Its per-HEAD cache is unchanged. It is skipped under an explicit --observed-fork-base, which outranks an inference from a prior worktree; - --observed-fork-base <sha> threads a measured fork base through the evaluation (strict full-hex, TypeError otherwise). Tests: the #4868 rows are unchanged and still reachable (they run with the setting unset); the #4752 rows re-land, with the one exit-128 row updated to the degrade #4734 pinned since; a new #4881 block pins the start-of-run state (baseref-head, git never consulted), the hook + observation composition in both directions, and that an explicit observation skips the probe. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RPXQQPzHGintWtbhBoQCRS * docs(#4881): rewrite the eight surfaces that still state the harness ignores worktree.baseRef:"head" Every prose surface #4868 left untouched still asserted the retired #48 premise as verified fact, starting with the step file the orchestrator reads. Each now describes the measured behaviour, the WorktreeCreate-hook exception, the --observed-fork-base input, and the #4868 observation as what lifts the hook degrade; docs/CLI-TOOLS.md gains the fork-from-head-observed reason row #4868 did not document. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RPXQQPzHGintWtbhBoQCRS * chore(#4881): set changeset fragment pr to 4921 * fix(#4881): withhold the #4868 observation under the hook interlock The hook interlock this PR added withheld the worktree.baseRef:"head" trust but still let b2's prior-worktree observation restore it, and that observation cannot be attributed to the hook. The evidence is a clean agent worktree sitting at the orchestrator HEAD; nothing on disk records which creator left it there, so one the plain harness created BEFORE a WorktreeCreate hook was configured — with HEAD unmoved since — reads as evidence for the hook. It was the one fail-open branch in a mechanism documented as fail-closed. Keying the observation cache by hook configuration does not close it. observeHarnessForkFromHead has two legs: a HEAD-keyed cache and a live probe over .claude/worktrees/agent-*. A hook-keyed cache simply misses, and the miss falls through to the probe, which re-finds the same stale worktree and re-confirms. The probe takes no hook input at all. So the observation is not consulted under the interlock rather than re-keyed: on a hook host the only admissible positive signal is an explicit --observed-fork-base measurement of the dispatch in hand, and absent one the inferred comparison runs and a mismatch degrades with baseref-head-bypassed-by-hook, leaving the spawn-time exit-42 guard as the backstop. Scoped to the case branch a. declined to trust: "head" set AND a hook (or an unparseable layer) in the harness's path. With no "head" setting b2 is #4868's own arm and is unchanged, hook or not — re-scoping that trust is a separate question this PR does not open, and a test pins the boundary. Cost, stated: a hook host with "head" set, on a branch diverged from origin/HEAD and passing no observation, now runs sequentially. It still runs parallel when HEAD matches origin/HEAD. No workflow threads an observation today. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuT9wTyrMaxjAPevqeH4YZ * refactor(#4881): drop the now-unused forkRef message-builder parameter buildMsgBaserefHeadIgnored stopped reading forkRef when the message was made mode-neutral, and the parameter was retained with `void forkRef;` for symmetry with its two sibling builders, which do read it. Symmetry is not reason enough to keep a dead parameter on a module-private function with one caller, so drop it (#4921 review). Behaviour is unchanged; the message text is pinned by an existing full-string assertion, which is what covers the only real risk here — transposing the two remaining arguments at the call site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuT9wTyrMaxjAPevqeH4YZ * test(#4881): exercise both FULL_SHA_RE alternatives at their boundaries The invalid-observation list pinned 39 and 41 hex around the 40-hex SHA-1 arm but left the 64-hex SHA-256 arm's own +/-1 boundary unexercised, which the repo's boundary-coverage convention asks for (#4921 review). Adds 63, 65 and a 64-length non-hex string. The regex already rejected all three -- this is coverage of correct behaviour, not a fix -- so it carries no negative control against a pre-fix base. That it is not vacuous was shown instead by widening the arm to {63,65}, under which the row fails by name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuT9wTyrMaxjAPevqeH4YZ * docs(#4881): finish the surface sweep the hook interlock owes Self-found by this round's own pre-push adversarial review, over six passes. Eight sites, four classes. FIVE were prose still asserting the stance the interlock overturned, which reads as live canon to anyone arriving cold: docs/CLI-TOOLS.md, docs/CONFIGURATION.md and gsd-core/references/planning-config.md each still said the hook degrade is lifted "unless/until a clean prior harness worktree is observed"; observeHarnessForkFromHead's own header still said a qualifying worktree "can only exist if the harness forked from HEAD"; and a test name still called the observation "required on a hook host" when it is now inadmissible there. My own sweep had grepped for "restores"/"lifts" and missed every one -- the ordinary failure of a grep, which returns what you thought to search for. The SIXTH is the same class one step worse: gsd-core/workflows/execute-plan.md still said flatly that Claude Code's isolation="worktree" "forks from origin/HEAD, not live local HEAD" -- in a paragraph THIS PR already edits, a few sentences after the clause it corrected. A tombstone makes only its own line clean; adjoining text asserting the dead stance is the other half of the same defect. Now qualified on the setting, with the hook exception named. The SEVENTH is a proof-strength overstatement that predates this PR, with a driven counterexample: a worktree created from an older base and since `git checkout --detach`ed onto HEAD is clean, sits at HEAD, and satisfies the probe identically, so "can only exist" was false. The worktree's own reflog does retain that original checkout -- the information is not lost, the probe simply does not consult it. The EIGHTH is that the header described only one of the function's two legs. A cache hit returns the prior conclusion without reading any worktree, so "the probe reads a worktree's present state" was true of the live probe and false of the cache. The header now separates them, and names the cache's blindness as a third reason the observation is inadmissible under a hook. Gaps seven and eight are inherited from #4868 and accepted there for the no-hook case. Nothing about the mechanism changes here; only what the header claims for it. No behavioural change -- comments, prose, and one test's registered name. Emitted-Drift-Ack-Growth: execute-plan.md — the Pattern A paragraph gained a qualifying clause: it stated flatly that Claude Code forks from origin/HEAD, which is the premise this PR retires, a few sentences after the clause the PR had already corrected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuT9wTyrMaxjAPevqeH4YZ --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
11 KiB
How to fix the worktree base-mismatch (exit 42) error
Goal: Understand why /gsd-execute-phase or /gsd-quick halts with FATAL: worktree base mismatch / exit 42 when your branch is ahead of the default branch, and choose the right fix to restore normal — or parallel — execution.
Prerequisites: GSD Core is installed and you have an active project. You have run /gsd-execute-phase or /gsd-quick and either seen the exit-42 error or the one-line ⚠ Worktree base mismatch warning.
What you will see
When you run /gsd-execute-phase or /gsd-quick on a branch that is ahead of the repository's default branch (for example, an unmerged milestone branch, a long-lived feature branch, or a branch with commits not yet in origin/HEAD), you may see one of two messages:
Automatic-degrade warning (phase or quick task still completes):
⚠ Worktree base mismatch: HEAD (abc12345) differs from origin/HEAD (def67890).
Running this phase sequentially on the main working tree. Parallel worktrees
return once HEAD is merged/pushed so origin/HEAD matches it, or set
worktree.baseRef:"head" to fork worktrees from HEAD instead (honored by
GSD-created worktrees and by the Claude Code harness; #683, #4588).
The phase or quick task runs to completion sequentially; nothing is blocked. This is the runtime mitigation (/gsd-execute-phase: #683/#1369; /gsd-quick: #1941).
Exit-42 halt (older installs or misconfigured environments):
FATAL: worktree base mismatch
All worktree-isolated executors halt immediately. Zero progress is made.
Why this happens
Unless worktree.baseRef is set to "head", Claude Code's isolation="worktree" forks executor worktrees from the repository's default branch (origin/HEAD), not from your current HEAD. When your branch contains commits that origin/HEAD does not have — plan files, new source files, anything added since the branch diverged — those files are absent inside each worktree. GSD's worktree-branch-check safety guard correctly refuses to act on a worktree that does not match the orchestrator's state, and exits with code 42.
This is the guard working as designed: it prevents silent data loss or phantom edits in the wrong tree. The error is a branch-state condition, not an OS-specific or hardware issue.
Option 1 — Do nothing (you are already unblocked)
If you saw the ⚠ Worktree base mismatch warning rather than an exit-42 halt, GSD has already automatically degraded to sequential execution on the main working tree for this run. The phase will complete. No action is required.
Use this option when:
- You are on a diverged branch temporarily
- You do not care about parallel execution for this phase
- You want to merge back to the default branch soon
Option 2 — Set worktree.baseRef: "head" (restores parallel execution on a diverged branch)
What this setting does (#3659, #4588): it makes new worktrees fork from your current HEAD
instead of origin/HEAD, so the files your branch added are present inside each executor
worktree and the wave can run in parallel. It is honored by GSD-created worktrees by construction
and, as measured, by Claude Code's harness:
- Runtimes where GSD itself runs
git worktree add <path> <start-point>(Codex, OpenCode, Kimi, Kimi Code) — by construction; the check suppresses on it (reason: "baseref-head"). - Claude Code's
Agent(isolation="worktree")— because the harness reads the setting and forks fromHEAD. This was measured on current Claude Code from the project-local, project-shared and user/global settings layers on macOS, Windows and Linux (#4588). Cursor, the other runtime whose harness creates the worktree, has not been measured — see below. An earlier finding that the harness did not read it (#48, verified against the Claude Code of the time; upstream claude-code#44965) was fixed upstream in claude-code#54940, but until #4588 GSD's pre-dispatch check still assumed it and degraded every wave on an unmerged branch regardless of the setting — thebaseref-head-ignored-by-harnessreason you may have seen on older installs.
Run the convenience command from your project root:
node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" worktree set-baseref
This writes worktree.baseRef: "head" into .claude/settings.local.json in your project root. It is no-clobber: if you already have an explicit baseRef set to something else, it leaves your value in place and tells you.
To verify the result, pass the dispatch mode the runtime uses:
node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" worktree base-check --mode harness-worktree
The output is JSON. With the setting in place expect shouldDegrade: false with
reason: "baseref-head" in either mode — the check does not compare HEAD against
origin/HEAD when the fork base is HEAD by configuration. Without the setting, on a diverged
branch, expect shouldDegrade: true with reason: "head-diverged-from-fork".
If you configure a Claude Code WorktreeCreate hook, the setting does not reach the worktrees
Claude Code dispatches: the hook creates them from the directory it emits, and Claude Code does not
apply worktree.baseRef on that path. The check looks for such a hook in the same three settings
files it reads worktree.baseRef from. When it finds one, or when one of those files does not
parse, it compares HEAD against origin/HEAD as if the setting were absent, and a mismatch
returns shouldDegrade: true with reason: "baseref-head-bypassed-by-hook" and a message naming
the file (#4588). That fallback is the comparison the check makes without the setting, and it does
not know where your hook forks either: when HEAD matches origin/HEAD the check does not degrade,
and the exit-42 guard below still catches a hook that forks elsewhere. A hook cannot prove itself by
leaving a worktree behind: the #4868 observation looks for a clean agent worktree under
.claude/worktrees/ sitting at the current HEAD, but nothing on disk records which creator left
it there, so one the plain harness created before you configured the hook — with HEAD unmoved
since — would read as evidence for the hook. The check therefore does not consult that observation
once a hook is in the path (#4881). To get a trusted verdict, pass the commit a hook-created
worktree starts at as --observed-fork-base (described next), or remove the hook. Hooks from managed policy settings, a --settings file, plugins, agent frontmatter or SDK
registrations are not visible to the check; there the exit-42 guard below is the backstop.
If you have a real measurement of what a worktree on this host forked from (git rev-parse HEAD
inside a freshly created isolated worktree, before any commit — the full sha, not an abbreviation),
pass it and the check evaluates that instead of inferring:
node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" worktree base-check --observed-fork-base <sha>
A match returns reason: "observed-fork-matches-head"; a mismatch with "head" set returns
reason: "baseref-head-ignored-by-harness" — that worktree was not forked from HEAD despite the
setting — and the run degrades to sequential as before.
Measured on Claude Code only. Cursor also declares harness-created worktrees; whether it honors
worktree.baseRef has not been measured. There the pre-dispatch check trusts the setting the same
way, and a host that does not honor it is caught by the exit-42 guard below. If you are on Cursor
and see exit 42 with the setting in place, the SHA the halted executor prints is exactly the
observation to pass here, and worth reporting.
Alternatively, set the value by hand in .claude/settings.local.json:
{
"worktree": {
"baseRef": "head"
}
}
Note: Fresh installs and upgrades of GSD Core both set worktree.baseRef:"head" automatically in .claude/settings.local.json (no-clobber) when workflow.use_worktrees is enabled (the default). If you see the degrade warning on a fresh install, check that the key is still present — a hand-edited settings.local.json is the usual reason it is not.
Use this option when:
- You regularly work on long-lived or milestone branches, on any worktree-capable runtime
- You want parallel phase execution there (faster, lower context-window pressure)
Option 3 — Fallback: disable worktrees entirely
If worktrees are causing persistent problems beyond the base-mismatch (for example, your environment does not support them), disable them permanently for this project:
Add or edit .planning/config.json:
{
"workflow": {
"use_worktrees": false
}
}
All executor agents will then run sequentially on the main working tree for every phase. This is equivalent to what the automatic degrade does, but permanent.
Use this option when:
- Worktrees are consistently problematic in your environment
- You prefer sequential execution for auditability or tooling reasons
- You are on a platform or CI setup that does not support git worktrees
See also: workflow.use_worktrees in the configuration reference.
The exit-42 backstop
The worktree-branch-check guard (exit 42) remains active in all execution modes as a safety backstop. It fires only when an executor worktree's branch does not match the expected orchestrator state — a condition that should not arise once you have applied one of the options above. It is also the observation-based check behind Option 2: with worktree.baseRef: "head" set, the pre-dispatch check trusts the setting and does not compare, so a host that does not honor it is caught here — the mismatched executors of that dispatch halt (every one of them, in a concurrent wave), and nothing is merged. If you continue to see exit 42 after setting worktree.baseRef: "head", your runtime is forking from somewhere other than HEAD; run /gsd-forensics to investigate, and pass the SHA the halted executor printed as --observed-fork-base to worktree base-check to see the verdict GSD would reach with that measurement.
Summary
| Situation | Recommended action |
|---|---|
| Saw the warning, phase completed | Nothing — degrade handled it automatically |
| Regularly on diverged branches, want parallel execution | worktree set-baseref (Option 2) |
| Worktrees consistently problematic | Set workflow.use_worktrees: false (Option 3) |
| Still seeing exit 42 after fixes | Run /gsd-forensics "exit 42 after fix" |