fix(#4881): trust worktree.baseRef:"head" in harness mode; a WorktreeCreate hook withholds it and only a measured fork base restores it (#4921)

* 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>
This commit is contained in:
0xdhx
2026-09-23 19:15:28 -05:00
committed by GitHub
parent d7b5b2c2b6
commit 238bee7b03
12 changed files with 1292 additions and 174 deletions

View File

@@ -1287,19 +1287,26 @@ Diagnose and configure the worktree fork base used by Claude Code's `isolation="
# Returns JSON: { shouldDegrade, reason, message, headSha, forkRef, forkSha }
node gsd-tools.cjs worktree base-check
# Same check, but against the fork base a worktree this host created was
# actually observed to have (git rev-parse HEAD inside it, before any commit).
node gsd-tools.cjs worktree base-check --observed-fork-base <sha>
# Write worktree.baseRef:"head" into .claude/settings.local.json (no-clobber).
# Returns JSON: { changed, skipped, previous, baseRef, file }
node gsd-tools.cjs worktree set-baseref
```
**`worktree base-check`** reads `worktree.baseRef` from a three-layer cascade — `.claude/settings.local.json`, then `.claude/settings.json`, then the user/global `settings.json` under `CLAUDE_CONFIG_DIR` (or `~/.claude`) — and compares the current `HEAD` SHA against `origin/HEAD`. Project-level settings take precedence over the user/global layer, so a machine-wide `worktree.baseRef:"head"` set via `/config` is honored when no project override exists. The `shouldDegrade` field is `true` when the execute-phase orchestrator will fall back to sequential execution. `--mode` declares who creates the isolated worktree (#3659): `harness-worktree` (the default — the runtime harness forks it and does **not** read project-settings `baseRef`, #48) or `orchestrator-worktree` (GSD itself runs `git worktree add` with an explicit start-point and honors `"head"`); invalid values fail closed with an error. Possible `reason` values:
**`worktree base-check`** reads `worktree.baseRef` from a three-layer cascade — `.claude/settings.local.json`, then `.claude/settings.json`, then the user/global `settings.json` under `CLAUDE_CONFIG_DIR` (or `~/.claude`) — and compares the current `HEAD` SHA against `origin/HEAD`. Project-level settings take precedence over the user/global layer, so a machine-wide `worktree.baseRef:"head"` set via `/config` is honored when no project override exists. The `shouldDegrade` field is `true` when the execute-phase orchestrator will fall back to sequential execution. `--mode` declares who creates the isolated worktree (#3659): `harness-worktree` (the default — the runtime harness forks it) or `orchestrator-worktree` (GSD itself runs `git worktree add` with an explicit start-point). `worktree.baseRef:"head"` is honored by the orchestrator by construction and by the Claude Code harness as measured from all three settings layers (#4588; the #48 finding that the harness did not read the setting predates upstream claude-code#54940); Cursor, the other `harness-worktree` host, is unmeasured. With `"head"` set the check does not compare in either mode unless `--observed-fork-base` is given (below), with one further exception under `harness-worktree` and no observation: when a Claude Code `WorktreeCreate` hook is configured in any of the same three settings files, or one of them does not parse, the hook creates the agent worktree and Claude Code does not apply `worktree.baseRef` to it, so the check compares `HEAD` against `origin/HEAD` as if the setting were absent (#4588). The #4868 prior-worktree observation is **not** consulted on that path: a worktree sitting at `HEAD` records nothing about which creator made it, so one the plain harness left there before the hook was configured would read as evidence for the hook (#4881). Pass `--observed-fork-base` for a trusted verdict on a hook host. Hooks from managed policy settings, a `--settings` file, plugins, agent frontmatter or SDK registrations are not visible to the check; the exit-42 guard remains the backstop for those. `--observed-fork-base <sha>` supplies the fork base a worktree created for a dispatch was actually measured to have (`git rev-parse HEAD` inside it, before any commit — a full 40- or 64-hex sha, case-insensitive; abbreviations are refused because the comparison is exact); when given, it replaces the `origin/HEAD` inference as the fork side of the comparison and `"head"` no longer short-circuits — the verdict then reports a measurement, and a mismatch under `"head"` means the worktree was not forked from HEAD despite the setting. Invalid values for either flag fail closed with an error. Possible `reason` values:
| `reason` | `shouldDegrade` | Meaning |
|---|---|---|
| `baseref-head` | `false` | `worktree.baseRef:"head"` is set and `--mode orchestrator-worktree` declares GSD-managed worktrees — the fork base is the orchestrator HEAD by construction |
| `baseref-head-ignored-by-harness` | `true` | `worktree.baseRef:"head"` is set but HEAD differs from `origin/HEAD` in harness (default) mode — the harness does not read the setting (#48), so the run degrades to sequential (#3659) |
| `head-matches-fork` | `false` | HEAD and `origin/HEAD` are the same commit |
| `head-diverged-from-fork` | `true` | Branch is ahead of or diverged from `origin/HEAD` |
| `baseref-head-bypassed-by-hook` | `true` | `worktree.baseRef:"head"` is set under `harness-worktree` with no fork base observed, but a Claude Code `WorktreeCreate` hook is configured in one of the three settings files (or one of them does not parse, so a hook cannot be ruled out), and `HEAD` differs from the inferred fork base. The hook creates the agent worktree and Claude Code does not apply `worktree.baseRef` to it, so the setting is not trusted; `message` names the file (#4588). The #4868 prior-worktree observation does **not** lift it: a worktree sitting at `HEAD` carries no record of which creator left it there, so one the plain harness created before the hook was configured is indistinguishable from one the hook created (#4881). Only `--observed-fork-base` restores a trusted verdict here. The fallback comparison is not a measurement of the hook: when `HEAD` matches the inferred fork base the check does not degrade, and the exit-42 guard remains the backstop. `--observed-fork-base` gives a measured verdict |
| `baseref-head` | `false` | `worktree.baseRef:"head"` is set and no fork base was observed (and, under `harness-worktree`, no `WorktreeCreate` hook was found in the settings files) — the fork base is the orchestrator HEAD in either mode (by construction under `orchestrator-worktree`; under `harness-worktree` as measured on Claude Code, #4588 — Cursor is unmeasured). The spawn-time `worktree-branch-check` exit-42 guard remains the backstop on a host that does not honor it |
| `baseref-head-ignored-by-harness` | `true` | `worktree.baseRef:"head"` is set but the fork base passed via `--observed-fork-base` differs from HEAD — the worktree was not forked from HEAD despite the setting (in either mode), so the run degrades to sequential (#3659, #4588) |
| `observed-fork-matches-head` | `false` | The fork base passed via `--observed-fork-base` equals HEAD (#4588) |
| `fork-from-head-observed` | `false` | Under `harness-worktree` with no `--observed-fork-base`, a clean linked worktree under `.claude/worktrees/agent-*` sits at the current `HEAD` — positive evidence that this host's worktree creator forks from `HEAD` (#4868). Reached only when `"head"` is absent or not `"head"` — with the setting trusted the check returns `baseref-head` first, and under a `WorktreeCreate` hook with `"head"` set the observation is withheld rather than consulted, because it cannot be attributed to the hook (#4881). Cached per `HEAD` in `.gsd/harness-fork-probe.json`; a dirty worktree, one at another commit, none at all, or a git failure is inconclusive and falls through |
| `head-matches-fork` | `false` | HEAD and the inferred fork base (`origin/HEAD`, or its symbolic-ref fallback such as `origin/next` — reported in `forkRef`) are the same commit |
| `head-diverged-from-fork` | `true` | Branch is ahead of or diverged from the fork base — the inferred `origin/HEAD` (or its fallback), or the `--observed-fork-base` value when one was given (`forkRef: "observed"`) |
| `fork-ref-unknown` | `true` | `origin/HEAD` could not be resolved |
| `no-head` | `true` for exit 128, `false` for exit 0 with empty stdout | Exit 128 is git's definitive "no resolvable HEAD here" answer — not a git repository, or a repository with no commits; no harness worktree can be created, so the check degrades to sequential (#4734), with a `message` explaining why. Exit 0 with empty stdout is ambiguous (git completed without a definitive answer) and stays non-degrading (`headAbsenceVerified` distinguishes the two: `true` / `false`) |
| `head-unresolvable` | `true` | `git rev-parse HEAD` did not return a definitive answer (timed out, `git` missing, or any other non-128 failure) — fails closed rather than being treated as `no-head` |

View File

@@ -566,7 +566,7 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `worktree.baseRef` | string | (unset) | Controls which ref the worktree-based parallel executor uses as the base when creating new phase/wave worktrees. When unset, the executor bases new worktrees on the repository default branch (`origin/HEAD`); if the current branch has diverged, execute-phase auto-degrades to sequential execution rather than halting (as of v1.4.0). Set to `"head"` to base new worktrees on the local `HEAD` instead. **Where it applies (#48/#3659):** honored on runtimes where GSD itself creates the worktrees (Codex, OpenCode, Kimi, Kimi Code) — there it restores wave-based parallel execution on diverged branches. On harness-isolated runtimes (Claude Code, Cursor) the harness does **not** read this setting (verified 5/5 in #48; upstream claude-code#44965): the base check compares against the real fork base regardless and auto-degrades to sequential execution before dispatch when `HEAD` has diverged, so the exit-42 halt is a last-resort backstop rather than the only guard. See [Fix the worktree base-mismatch (exit 42) error](how-to/fix-worktree-base-mismatch.md). |
| `worktree.baseRef` | string | (unset) | Controls which ref the worktree-based parallel executor uses as the base when creating new phase/wave worktrees. When unset, the executor bases new worktrees on the repository default branch (`origin/HEAD`); if the current branch has diverged, execute-phase auto-degrades to sequential execution rather than halting (as of v1.4.0). Set to `"head"` to base new worktrees on the local `HEAD` instead — this restores wave-based parallel execution on diverged branches. **Where it applies (#3659/#4588):** honored on runtimes where GSD itself creates the worktrees (Codex, OpenCode, Kimi, Kimi Code) by construction, and on Claude Code by its harness — measured from the project-local, project-shared and user/global layers on macOS, Windows and Linux (#4588). Cursor also declares harness-created worktrees but has not been measured; there the check trusts the setting the same way and the exit-42 guard is the backstop. (#48 had found the harness did not read the setting; that was fixed upstream in claude-code#54940, and until #4588 the base check still assumed it, degrading every wave on an unmerged branch regardless of the setting.) With `"head"` set the pre-dispatch base check trusts it and does not compare, with two exceptions: a supplied `--observed-fork-base` is compared against HEAD instead, and on a harness-created run with no observation a Claude Code `WorktreeCreate` hook in one of those three settings files — or one that does not parse — withholds the trust and the check compares against `origin/HEAD` (#4588). The #4868 prior-worktree observation does not lift that: nothing on a worktree records which creator made it, so one the plain harness left at `HEAD` before the hook was configured would read as evidence for the hook — pass `--observed-fork-base` for a trusted verdict there (#4881). The spawn-time exit-42 guard in each executor remains the observation-based backstop on a host that does not honor the setting. See [Fix the worktree base-mismatch (exit 42) error](how-to/fix-worktree-base-mismatch.md). |
### Executor isolation per runtime

View File

@@ -15,9 +15,9 @@ When you run `/gsd-execute-phase` or `/gsd-quick` on a branch that is ahead of t
```
⚠ 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.
(worktree.baseRef:"head" applies only where GSD itself creates the worktree —
the runtime harness does not read it; #48, #3659.)
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).
@@ -34,7 +34,7 @@ All worktree-isolated executors halt immediately. Zero progress is made.
## Why this happens
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.
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.
@@ -52,20 +52,23 @@ Use this option when:
---
## Option 2 — Where `worktree.baseRef: "head"` actually applies (runtimes where GSD creates the worktrees)
## Option 2 — Set `worktree.baseRef: "head"` (restores parallel execution on a diverged branch)
**What this setting can and cannot do (#48, #3659):** on runtimes whose own harness creates isolated
worktrees (Claude Code's `Agent(isolation="worktree")`), the harness forks from the repository
default branch and **does not read project-settings `baseRef`** — verified 5/5 in #48; tracked
upstream at claude-code#44965/#43535. On those runtimes, setting `baseRef:"head"` does **not**
restore parallel worktrees on a diverged branch, and since #3659 it no longer silences the
pre-dispatch check: GSD compares `HEAD` against the real fork base and auto-degrades to sequential
execution before dispatch instead of letting every executor die at the exit-42 guard.
**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:
The setting **is** honored where GSD itself runs `git worktree add <path> <start-point>` — the
orchestrator-managed isolation used on runtimes with a headless exec surface (Codex, OpenCode,
Kimi, Kimi Code). There `baseRef:"head"` really does fork from your current `HEAD`, the check
suppresses on it (`reason: "baseref-head"`), and parallel execution works on any branch.
- **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 from `HEAD`. 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 — the `baseref-head-ignored-by-harness`
reason you may have seen on older installs.
Run the convenience command from your project root:
@@ -75,18 +78,51 @@ 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 on a harness-isolated runtime (Claude Code, Cursor), pass the dispatch mode so
the check evaluates the base the harness will actually use:
To verify the result, pass the dispatch mode the runtime uses:
```bash
node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" worktree base-check --mode harness-worktree
```
The output is JSON. On a diverged branch expect `shouldDegrade: true` with
`reason: "baseref-head-ignored-by-harness"` — GSD will run the phase sequentially; parallel
worktrees return once `HEAD` is merged/pushed so `origin/HEAD` matches it. On a GSD-managed runtime,
`--mode orchestrator-worktree` returns `shouldDegrade: false` with `reason: "baseref-head"` and
parallel worktrees work on any branch.
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:
```bash
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`:
@@ -98,11 +134,11 @@ Alternatively, set the value by hand in `.claude/settings.local.json`:
}
```
**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). This remains useful for GSD-managed runtimes and harmless elsewhere — post-#3659 it never silences the check on harness-managed ones.
**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:
- Your runtime uses GSD-managed worktrees (Codex, OpenCode, Kimi, Kimi Code) and you regularly work on long-lived or milestone branches
- 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)
---
@@ -135,7 +171,7 @@ See also: [`workflow.use_worktrees`](../CONFIGURATION.md#workflow-toggles) in th
## 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. If you continue to see exit 42 after setting `worktree.baseRef: "head"`, run `/gsd-forensics` to investigate.
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.
---