fix(hooks): judge the branch the push actually comes from - #286
Open
VasiHemanth wants to merge 1 commit into
Open
fix(hooks): judge the branch the push actually comes from#286VasiHemanth wants to merge 1 commit into
VasiHemanth wants to merge 1 commit into
Conversation
Both pre-push hooks resolved the repo with `git rev-parse --show-toplevel` from their own process cwd, which is the session's project root. That is not where the push runs once Claude is in a worktree or the command starts with `cd <dir> &&`, so a worktree push was evaluated against whatever branch the project root happened to be on. Concretely: a worktree branch carrying only `fix:` commits was denied because the project root sat on a `feat:` branch whose UPDATE.json change was still uncommitted. A worktree checked out on `main` was denied too, despite the hook's explicit "skip on main" rule. Adds `resolve_push_cwd()` to enforce-update-json.py and uses it in both hooks. Precedence, most explicit first: `git -C <dir>` in the push fragment, the accumulated `cd <dir>` prefix of the same chain, the payload's `cwd` field (the worktree root after EnterWorktree), then the hook's own cwd. Relative paths compose. Unresolvable forms (`cd -`, `cd ~...`) fall back rather than guess. Non-worktree pushes are unaffected: the payload cwd equals the process cwd, so every path resolves exactly as before. backend/test_hooks.py covers the resolver and drives the hook end to end over real throwaway repos and worktrees. Eleven of the fifteen fail against the unfixed hooks, including the two that assert a fix-only worktree and a main worktree are allowed, while the guard tests (feat: without UPDATE.json denied, in a worktree too) pass either way. Co-Authored-By: Claude Opus 5 (1M context) <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.
Summary
Both pre-push hooks resolved the repo with
git rev-parse --show-toplevelfrom their own process cwd, which is the session's project root. That is not
where the push runs once Claude is in a worktree, or once the command starts
with
cd <dir> && ..., so a worktree push was judged against whatever branchthe project root happened to be sitting on.
This repo does most of its work in worktrees, so the hooks were routinely
answering about the wrong branch.
What went wrong in practice
A worktree branch carrying only
fix:commits was denied, because the projectroot sat on a
feat:branch whoseUPDATE.jsonchange was still uncommitted.Per
.claude/CLAUDE.mdcase 1, a purefix:branch should have been allowedsilently.
The same path also denied a worktree checked out on
main, despite the hook'sexplicit "skip enforcement if we're on main" rule, because the branch it read
was never
mainto begin with.The fix
Adds
resolve_push_cwd()toenforce-update-json.py; both hooks call itbefore asking git anything. Precedence, most explicit first:
git -C <dir>in the push fragment itselfcd <dir>prefix of the same command chaincwdfield, which is the worktree root after Claude enters aworktree
Relative paths compose against whatever is current at that point. Forms that
can't be resolved to a concrete path (
cd -,cd ~/x) fall back rather thanguess.
Non-worktree pushes are unaffected: the payload cwd equals the process cwd, so
every path resolves exactly as it does today.
Type of change
How to test
backend/test_hooks.py(new, 15 tests) covers the resolver directly anddrives
enforce-update-json.pyend to end as a subprocess with a realPreToolUse payload, over throwaway repos and real
git worktreecheckouts.Eleven of the fifteen fail against the unfixed hooks, including the two
behavioural ones (
test_worktree_push_is_judged_on_its_own_branch,test_main_branch_is_always_allowed). The guard tests pass either way, so thefix doesn't loosen the gate:
test_denies_feat_branch_without_update_jsontest_worktree_feat_branch_still_denied(a realfeat:worktree is still denied)test_allows_when_update_json_is_touchedtest_push_detection_unchangedVerified against this repo as well: the previously-denied push from a
fix:-only worktree is now allowed, and the livefeat/local-model-insightsbranch is still correctly denied.
Full backend suite: 461 passed, with the same pre-existing failure set as
main(22, all unrelated local-config pollution intest_power_config/test_pricing/test_update_check).Checklist
Related issue
N/A. Found while pushing a follow-up to #285 from a worktree.
🤖 Generated with Claude Code