feat(hooks): branch-guard — keep worktree-flow primary clones on a base branch - #86
Merged
Conversation
…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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
Adds a
branch-guardPreToolUse(Bash) hook that blocks agit checkout/git switchwhich would move a worktree-flow repo's PRIMARY clone onto a non-base feature branch, steering todevflow worktree. Also hardensdevflow worktreeagainst 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
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.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..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).gitPATH 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 worktreepath hardeningworktrunk sometimes silently ignores its
worktree-pathtemplate and drops the new worktree as a sibling of the repo.devflow worktreenow 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
lib/hooks/branch-guard.pylib/hooks/branch-guard.shlib/init.shPreToolUse(Bash) hooklib/worktree.sh_expected_worktree_root+_fix_worktree_locationREADME.mdValidation
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_compileclean;_register_settings hooksreally adds the PreToolUse(Bash) entry;_expected_worktree_rootreads the configured root from worktrunk config.make skills-check+make flows-checkpass.🤖 Generated with Claude Code