Skip to content

feat(hooks): branch-guard — keep worktree-flow primary clones on a base branch - #86

Merged
AndreJorgeLopes merged 2 commits into
mainfrom
feat/branch-guard-hook
Jul 28, 2026
Merged

feat(hooks): branch-guard — keep worktree-flow primary clones on a base branch#86
AndreJorgeLopes merged 2 commits into
mainfrom
feat/branch-guard-hook

Conversation

@AndreJorgeLopes

Copy link
Copy Markdown
Owner

TL;DR

Adds a branch-guard PreToolUse(Bash) hook that blocks a git checkout/git switch which would move a worktree-flow repo's PRIMARY clone onto a non-base feature branch, steering to devflow worktree. Also hardens devflow worktree against worktrunk ignoring the configured path.

Why

When an agent parks a repo's main clone on a feature branch (instead of using a worktree), anything symlinked to that clone silently swaps to the branch's code. In practice a local-dev devflow plugin install (symlinked to the clone) served whatever branch a session happened to leave the clone on. A reflog check found 14 such in-clone feature checkouts in a single day. This hook enforces the intended model: the primary clone stays on a base branch, feature work lives in isolated worktrees.

What it does

  • Blocks git checkout -b / git switch -c / git checkout <existing-feature-branch> when the target dir is a repo's primary clone (not a linked worktree) and the repo uses the worktree flow.
  • Fail-open, never blocks: base branches (main/develop/staging/… + configurable), any checkout inside a linked worktree, path restores (git checkout -- file, .), git checkout -, non-worktree-flow repos, and anything it can't parse.
  • "Worktree-flow repo" = has .worktrunk.toml, OR has ≥1 linked worktree, OR lives under a configured enforce-root (so it correctly guards repos like this one that have worktrees but no .worktrunk.toml).
  • Governs the agent's git commands only (a Claude hook can't intercept your terminal; git has no blocking pre-checkout hook). Terminal/other-agent coverage would need a git PATH shim — deliberately out of scope here.

Config (optional, personal — not shipped)

~/.config/devflow/branch-guard.json (keep in your own dotfiles/yadm):

{ "off": false, "base_branches": [], "enforce_roots": ["~/dev"] }

Env overrides: DEVFLOW_BRANCH_GUARD_OFF=1, DEVFLOW_BRANCH_GUARD_BASE_BRANCHES=a,b, DEVFLOW_BRANCH_GUARD_ROOTS=/p1:/p2.

Bonus: devflow worktree path hardening

worktrunk sometimes silently ignores its worktree-path template and drops the new worktree as a sibling of the repo. devflow worktree now reads the configured root from worktrunk's config (never hardcodes a personal path), and if the worktree landed elsewhere, moves it to <root>/<repo>/<branch-slug> and reports it.

Changes

Path Change
lib/hooks/branch-guard.py Hook logic (parse git checkout/switch, clone-vs-worktree detection, decision)
lib/hooks/branch-guard.sh Thin wrapper (fail-open if python3 absent)
lib/init.sh Register branch-guard as a global PreToolUse(Bash) hook
lib/worktree.sh _expected_worktree_root + _fix_worktree_location
README.md Branch-guard + config docs

Validation

  • 14-case block/allow matrix all pass: blocks feature checkout -b/switch -c/compound/git -C; allows base branches, in-worktree checkouts, path restores, checkout -, non-flow repos, and shell noise (echo git checkout …).
  • bash -n + py_compile clean; _register_settings hooks really adds the PreToolUse(Bash) entry; _expected_worktree_root reads the configured root from worktrunk config.
  • make skills-check + make flows-check pass.

🤖 Generated with Claude Code

…se branch

Adds a PreToolUse(Bash) hook (lib/hooks/branch-guard.{sh,py}) that blocks a git
checkout/switch which would move a worktree-flow repo's PRIMARY clone onto a
non-base feature branch, steering to `devflow worktree`. Stops the churn where
an agent parks the main clone on a feature branch, silently swapping whatever is
symlinked to that clone (e.g. a local-dev plugin install) to the branch's code.

Fail-open: base branches, any checkout inside a linked worktree, path restores,
and non-worktree-flow repos are always allowed. A repo is worktree-flow if it has
.worktrunk.toml, has >=1 linked worktree, or lives under a configured enforce-root.
Personal config (optional, not shipped): ~/.config/devflow/branch-guard.json;
env overrides DEVFLOW_BRANCH_GUARD_*. Registered globally by `devflow init`.

Also hardens `devflow worktree`: after `wt switch --create`, read worktrunk's
configured root and, if the worktree landed elsewhere (template ignored), move it
under <root>/<repo>/<branch-slug>.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 28, 2026 18:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Lets a `git` wrapper reuse the exact same guard decision (parse + clone-vs-worktree
detection + flow-repo check) instead of duplicating it. `branch-guard.py --cli <cwd>
<git-args...>` exits 2 + a stderr message when the checkout/switch should be blocked,
else 0. Fail-open on anything unparseable so a shim can never wedge git.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AndreJorgeLopes
AndreJorgeLopes merged commit 54caf3d into main Jul 28, 2026
1 check passed
@AndreJorgeLopes
AndreJorgeLopes deleted the feat/branch-guard-hook branch July 28, 2026 18:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants