T-1CL4-worktree-scope-guard-pre-commit
Status: closed/done · Impact: medium · Complexity: small
Auto-generated from a /sdlc:task-work post-mortem. Review and
promote to open/ready before picking up.
A /sdlc:task-work sub-agent that operates from an absolute path
pointing at the main repo (instead of the worktree at
.claude/worktrees/<basename>/) silently lands edits in the wrong
tree. Recovery requires diff-and-revert and a re-apply via git apply, which costs context and risks losing edits if the agent
doesn’t notice. A mechanical guard would catch this at the first
commit attempt rather than mid-run. Surfaced by post-mortem of
T-6HFR-orchestrator-categorized-in-flight-limits.
task-work Step 6 briefs the implementation sub-agent with two
absolute paths: the worktree root and the task file inside it. The
plugin root, however, resolves to the main repo’s plugin checkout
— same physical path that exists inside every worktree. If the
sub-agent edits a file via the plugin-root path, the edit lands on
main instead of the worktree’s feat/<basename> branch, and
nothing observable goes wrong until a git status on either side
shows the mismatch.
Today there is no commit-time guard against this. The brief tells the sub-agent “use absolute paths everywhere” but doesn’t enforce it.
Proposed
Section titled “Proposed”A lefthook pre-commit hook on main (and only main) that scans
staged paths against the active set of .claude/worktrees/*/
basenames. If a staged path belongs to a file the active worktree
also owns (e.g. plugin/conventions/sdlc-yaml.md while
.claude/worktrees/2026-05-20-orchestrator-categorized-in-flight-limits/
exists), the hook fails the commit with a message naming the
worktree and the correct path to re-apply to.
The hook should NOT fail when there is no active worktree, when the
staged paths are inside docs/planning/tasks/ (those are
intentionally edited on main by task-work Steps 5b and 11a), or when
the user has explicitly opted out via an env var.
Approach
Section titled “Approach”- Add
plugin/scripts/worktree_scope_guard.py(NOT an inline lefthook command — the logic deserves its own testable script). It walks.claude/worktrees/*/, reads the staged paths viagit diff --cached --name-only, and exits non-zero if any staged path (outsidedocs/planning/tasks/) is owned by an active worktree. - Register it as a
pre-commithook inlefthook.ymlon the main repo (skip inside worktrees — they have their own feat branch and the guard would self-trigger). - Document the carve-out for
docs/planning/tasks/and theWORKTREE_SCOPE_GUARD=skipenv-var override inplugin/conventions/worktree-scope-guard.md.
Files to touch
Section titled “Files to touch”plugin/scripts/worktree_scope_guard.py(new) — the guard.lefthook.yml(new — lefthook is not yet adopted in this repo; introducing it as part of this task) — register the pre-commit step.plugin/conventions/worktree-scope-guard.md(new) — document the contract and theWORKTREE_SCOPE_GUARD=skipoverride.
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: Attempting
git commiton main with an edit to a file also present under.claude/worktrees/<basename>/(excludingdocs/planning/tasks/) is rejected with a message naming the worktree. - AC-2: Commits to
docs/planning/tasks/*.mdon main with an active worktree still succeed (Step 5b / Step 11a carve-out). - AC-3: Setting
WORKTREE_SCOPE_GUARD=skipbypasses the guard.
Out of scope
Section titled “Out of scope”- Auto-rewriting the path into the worktree on detection. The guard fails the commit; the user (or sub-agent) reapplies.
- Guarding push (the failed commit is the chokepoint).
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:task-work post-mortem of T-6HFR-orchestrator-categorized-in-flight-limits on 2026-05-21.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-21. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto — direct script invocation against a synthetic
collision in
/tmp/wtsg-test(stagedplugin/conventions/sdlc-yaml.mdwith a matching path under.claude/worktrees/example-task/) returned exit 1 and printed the worktree name and the re-apply path. - AC-2: auto — same scratch repo, staging only
docs/planning/tasks/example-task.mdreturned exit 0; theALLOWED_PREFIXEScarve-out fires before the collision check. - AC-3: auto —
WORKTREE_SCOPE_GUARD=skipagainst the AC-1 collision returned exit 0; the env-var short-circuit fires before any path scan.
What worked
Section titled “What worked”- The PEP-723 self-bootstrapping pattern (cribbed from
plugin/scripts/find_todos.py) meant norequirements.txt/pyproject.tomlchurn for one new script. start_task.pydid the right thing on the rebase conflict — exit 3 with the worktree left in REBASE state, exact instruction in the error message. Recovery was a one-line resolution.
Friction and automation gaps
Section titled “Friction and automation gaps”start_task.pyhit a rebase conflict on the task file because Step 5a’sreadiness_verified_at:stamp on the feat branch and Step 5b’slast_reviewed:bump on main both edit frontmatter on adjacent lines. The conflict is mechanical (always: take main’slast_reviewed, feat branch’sreadiness_verified_at,status: in-progressfrom main) and a script could auto-resolve it. There is already a related task on this exact friction —2026-05-20-task-work-rebase-frontmatter-conflict— so no new task needed here.- Lefthook is not installed on the maintainer’s machine and is not
a current project dependency.
lefthook.ymlis now committed, but the guard does not actually fire ongit commituntilbrew install lefthook && lefthook installruns. The skill has no way to verify “this guard is wired” — adding ajust setup-mainrecipe (or a checked-in bootstrap script) that runslefthook installand warns if the binary is missing would close this. AskUserQuestionis not available to a sub-agent dispatched by the parent orchestrator’s verdict-line contract, so the lefthook install gap (a real implementation choice the user could have weighed in on) was decided unilaterally. Worth noting for the parent: when an orchestrated task needs a design call, the sub-agent currently has to either guess or bail with BLOCKED. A pre-implementation “check for open questions” pass in the task-work brief would surface these before the sub-agent commits to a path.
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-RQU1-worktree-scope-guard-downstream-setup — created
in response to PR #78 review: wire the worktree-scope-guard
lefthook into
/sdlc:setupso downstream projects adopting the SDLC plugin get the protection automatically rather than hand-rollinglefthook.yml.