Skip to content

fix(source-control): the WorktreeCreate gate registers unconditionally at user scope, and its newly-reachable opt-out asserts an unverified harness behavior #2211

Description

@kyle-sexton

Two questions about this hook have the same answer surface: where does it run, and how do I turn it off. Today the answer to the first is "everywhere on the machine" — WorktreeCreate supports no matcher, so a user-scope install runs a git-only hook in every directory. The answer to the second only became reachable at HEAD (#2193 declared the option for the first time), and nobody has ever measured what it does — the test suite asserts an external harness behavior that no current doc confirms in either direction.

Rows

  • I9 SC-F1 (registration-scope half only)plugins/source-control/hooks/hooks.json — the WorktreeCreate hook is registered unconditionally at user scope on an event that supports no matcher, so a git-only gate runs in every directory on the machine. The deadlock half of this row is UPSTREAM and is filed separately (see cross-links) — do not chase it here.
  • I9 SC-F5plugins/source-control/hooks/worktree-create-gate.sh:46-47 and plugins/source-control/hooks/worktree-create-gate.test.sh:131-132 — the disabled path exits 0 with an empty stdout, and the suite asserts that Claude Code then uses its own default. That harness behavior is undocumented, and a green suite asserting a false external behavior is the worse half of the defect.
  • I9 SC-F6plugins/source-control/hooks/worktree-create-gate.sh:49-57, :63 — the disabled-path skip notice reaches nobody outside debug logging, by the event's construction. Self-documented with a # silent-skip-ok: marker; its correct resolution is SC-F5's, not its own.

Evidence

SC-F1 — registration is the only scoping seam, and it is unconditional

plugins/source-control/hooks/hooks.json:

"WorktreeCreate": [
  {
    "hooks": [
      {
        "type": "command",
        "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/worktree-create-gate.sh",
        "timeout": 60,
        "statusMessage": "Placing the worktree at the external root..."
      }
    ]
  }
]

No matcher, no condition — and the event supports none. https://code.claude.com/docs/en/hooks, fetched 2026-08-11: WorktreeCreate has "no matcher support" and "always fires on every occurrence". So registration genuinely is the only scoping seam, which is the structural claim that stands. The gate is git-only by delegation: hooks/worktree-create-gate.sh:144 calls scripts/worktree-create.sh, which at :258-261 does:

if ! toplevel=$(git -C "$repo_dir" rev-parse --show-toplevel 2>/dev/null); then
  printf '%s: --repo-dir is not inside a git repository: %s\n' "$PROG" "$repo_dir" >&2
  exit 4

CORRECTED MECHANISM — the originally-filed rationale is falsified. The finding as filed said the registration deletes the harness stand-down and creates a deadlock at a non-repo root. It does not. https://code.claude.com/docs/en/agent-view, "How file edits are isolated", fetched 2026-08-11: "When the hook fails in a directory that isn't a git repository, the session skips isolation for that directory and edits the working directory in place." The registration changes only how the stand-down is reached (attempt-then-skip instead of skip-up-front). The observed deadlock is a harness divergence, filed upstream. What survives here is a plain scope concern — a git-only hook firing machine-wide — graded MED and routed as optional hardening, not as a deadlock fix.

Fleet caveat that survives regardless: other installed plugins may also register WorktreeCreate (the machine carries ~65 plugins; not enumerated), so a registration-scope change here would not be fleet-complete even if adopted. Weigh that before spending on it.

SC-F5 — the opt-out asserts an unverified harness behavior, on a path that only just became live

plugins/source-control/hooks/worktree-create-gate.sh:62-65:

if [[ "${CLAUDE_PLUGIN_OPTION_WORKTREE_CREATE_GATE_ENABLED:-true}" != "true" ]]; then
  printf 'worktree-create-gate: disabled by worktree_create_gate_enabled=false; …\n' >&2
  exit 0
fi

The header at :46-47 states "Disabled means 'let Claude Code use its own default', which is exit 0 with an empty stdout". The suite locks that contract in at plugins/source-control/hooks/worktree-create-gate.test.sh:131-132:

assert_exit "disabled exits 0 so Claude Code uses its own default" 0 "$STATUS"
assert_silent "disabled prints no path — an empty stdout is what yields the default" "$OUT"

The doc leg that made this look certain is gone. The finding as filed cited a hooks-page sentence reading "Hook failure or missing path fails creation". That sentence is not in the current text. https://code.claude.com/docs/en/hooks was fetched three ways on 2026-08-11 (page, hooks.md, and a targeted request for the full decision-control table). The WorktreeCreate row now reads verbatim:

"WorktreeCreate | Exit code or hookSpecificOutput.worktreePath | Any non-zero exit code fails creation. For HTTP hooks, return 2xx with hookSpecificOutput.worktreePath to override the default path"

and the exit-code table row reads "Any non-zero exit code causes worktree creation to fail." No sentence containing "missing path", "no path", or "empty stdout" appears anywhere reachable. (https://code.claude.com/docs/en/hooks-reference returns HTTP 404.) So the behavior of a WorktreeCreate hook that exits 0 without printing a path is undocumented — not documented-as-failing. The harness outcome is unconfirmed in either direction, and this issue must not be read as asserting that the opt-out fails creation.

What is a defect on its own terms, regardless of which way the measurement goes: the test suite asserts an external harness behavior that no current doc confirms.

Why this matters more now than when it was audited. #2192 (CLOSED 2026-08-11, "Plugin options are undiscoverable, and two enablement paths do not work") and its fix #2193 (2fb6901d, source-control 0.51.7) declared worktree_create_gate_enabled in userConfig for the first time. At HEAD, plugins/source-control/.claude-plugin/plugin.json:43 declares it, and plugins/source-control/README.md:286 states that before this "the option was never declared here — so Claude Code never exported CLAUDE_PLUGIN_OPTION_WORKTREE_CREATE_GATE_ENABLED, the hook's :-true fallback always won, and the gate could not be turned off." The opt-out only became reachable at HEAD. 2fb6901d made the switch reachable; it did not verify what the switch does.

Correction to a related earlier grading, so it is not repeated. A sibling audit graded the undeclared option a "declaration/discoverability gap, not a lockout", reasoning that a settings env entry would still reach the hook's plain env read. That rationale is falsified: the shipped fix records the option was inert in both directions — the gate ran unconditionally and setting the option produced no effect and no error. Anyone citing this history must not repeat the "not a lockout" framing. (Confirmed by https://code.claude.com/docs/en/plugins-reference, fetched 2026-08-11: "All values are exported to hook processes as CLAUDE_PLUGIN_OPTION_<KEY> environment variables" — scoped to declared userConfig values, so an undeclared key yields no variable.)

The one command that settles SC-F5 — a pending operator action, not a blocker on this issue

In a throwaway git repo, set worktree_create_gate_enabled=false, trigger a real WorktreeCreate (claude --worktree probe), and record whether the worktree is created at Claude Code's default location or creation fails.

  • If it is created — the header comment and the two test assertions are correct, and SC-F5 closes.
  • If creation fails — correct worktree-create-gate.sh:46-47 and worktree-create-gate.test.sh:131-132 together.

Either way, do not document the opt-out as usable until this is run. File-and-measure, not block-and-wait: the issue stands on the SSOT defect (a suite asserting an unverified external behavior) whether or not anyone runs the probe.

SC-F6 — the skip notice is invisible by construction

plugins/source-control/hooks/worktree-create-gate.sh:49-57 carries the reasoning and a # silent-skip-ok: marker; the notice itself is the printf … >&2 at :63 on the exit 0 path. https://code.claude.com/docs/en/hooks, fetched 2026-08-11, verbatim: "Stderr from a hook that exits 0 goes to the debug log only, never the transcript, and Claude never sees it." A real observability gap, correctly self-documented, whose resolution follows from SC-F5's outcome rather than standing alone.

Provenance

Severity: MED (cluster highest; SC-F6 is LOW) · Provenance: AUDITOR_VERIFIED — for SC-F5, the exit-0/empty-stdout half is reproduced at HEAD; the harness-outcome half is inference and is unconfirmed in either direction. SC-F1's originally-filed rationale is falsified; the corrected mechanism is stated above.
Origin: handoff-inbox item 20260811-021645-plugin-audit-four-components-and-guard-deadlock-ownership
Ledger: .work/handoff-inbox-batch-4/ledgers/I9-021645-four-components.md § "Lane D — source-control / worktree-create-gate"
Verified against repo HEAD 685dd381.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions