Skip to content

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.

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.

  1. 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 via git diff --cached --name-only, and exits non-zero if any staged path (outside docs/planning/tasks/) is owned by an active worktree.
  2. Register it as a pre-commit hook in lefthook.yml on the main repo (skip inside worktrees — they have their own feat branch and the guard would self-trigger).
  3. Document the carve-out for docs/planning/tasks/ and the WORKTREE_SCOPE_GUARD=skip env-var override in plugin/conventions/worktree-scope-guard.md.
  • 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 the WORKTREE_SCOPE_GUARD=skip override.
  • AC-1: Attempting git commit on main with an edit to a file also present under .claude/worktrees/<basename>/ (excluding docs/planning/tasks/) is rejected with a message naming the worktree.
  • AC-2: Commits to docs/planning/tasks/*.md on main with an active worktree still succeed (Step 5b / Step 11a carve-out).
  • AC-3: Setting WORKTREE_SCOPE_GUARD=skip bypasses the guard.
  • 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).
  • none

Spawned by /sdlc:task-work post-mortem of T-6HFR-orchestrator-categorized-in-flight-limits on 2026-05-21.

Captured by /sdlc:task-work on 2026-05-21. PR: pending.

  • AC-1: auto — direct script invocation against a synthetic collision in /tmp/wtsg-test (staged plugin/conventions/sdlc-yaml.md with 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.md returned exit 0; the ALLOWED_PREFIXES carve-out fires before the collision check.
  • AC-3: auto — WORKTREE_SCOPE_GUARD=skip against the AC-1 collision returned exit 0; the env-var short-circuit fires before any path scan.
  • The PEP-723 self-bootstrapping pattern (cribbed from plugin/scripts/find_todos.py) meant no requirements.txt / pyproject.toml churn for one new script.
  • start_task.py did 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.
  • start_task.py hit a rebase conflict on the task file because Step 5a’s readiness_verified_at: stamp on the feat branch and Step 5b’s last_reviewed: bump on main both edit frontmatter on adjacent lines. The conflict is mechanical (always: take main’s last_reviewed, feat branch’s readiness_verified_at, status: in-progress from 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.yml is now committed, but the guard does not actually fire on git commit until brew install lefthook && lefthook install runs. The skill has no way to verify “this guard is wired” — adding a just setup-main recipe (or a checked-in bootstrap script) that runs lefthook install and warns if the binary is missing would close this.
  • AskUserQuestion is 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.
  • T-RQU1-worktree-scope-guard-downstream-setup — created in response to PR #78 review: wire the worktree-scope-guard lefthook into /sdlc:setup so downstream projects adopting the SDLC plugin get the protection automatically rather than hand-rolling lefthook.yml.

← Back to Tasks