fix(#549): block-main-push checks the operation's target branch, not session cwd#580
Conversation
…session cwd The hook previously resolved the current branch via `git branch --show-current` in the hook's cwd (the harness primary checkout). When the primary checkout sat on a protected branch (e.g. `dev`) while the actual operation targeted a feature-branch worktree, the hook false-blocked legitimate commits and pushes. Push path: reuse `_lib-extract-push-ref.sh` (the same lib `validate-branch-name.sh` already uses for the same worktree-cwd problem — #194, #547) to read the DESTINATION branch directly from the push command. Tag pushes remain no-ops. Falls back to local HEAD only when no explicit ref is present in the command (bare `git push` relying on upstream tracking). Commit path: detect the `cd <path> && git commit` compound shell pattern and resolve the branch of the TARGET worktree via `git -C <path> branch --show-current` instead of the session cwd. Plain `git commit` (no `cd` prefix) retains the original behaviour. Tests: 26 cases covering (a) push to protected still blocks, (b) push to feature passes when cwd=dev, (c) tag push passes, (d) cd wt && commit on feature-branch wt passes while cwd=dev, (e) plain commit on protected still blocks. Refs #549 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
atlas-apex
left a comment
There was a problem hiding this comment.
Code Review: PR #580
Commit: 656ae4629a3ef0ff6a04f7715499e391fba33e6e
Summary
Makes block-main-push.sh evaluate the operation's target branch instead of the session cwd's branch — push side reuses _lib-extract-push-ref.sh (dst-from-command + tag-push no-op + bare-push fallback to local HEAD), commit side resolves the target worktree via git -C <path> for the cd <path> && git commit compound shape. Adds a 26-case test suite (all green locally).
The direction of the fix is correct and the push-side logic is sound. However, the commit-side cd-path extraction has a false-negative that lets a real protected-branch commit through. Blocking on that.
Checklist Results
- ✅ Architecture & Design: Pass (correct approach — gate on the operation's target, mirrors the merge-gate/validate-branch-name pattern)
- ⚠ Code Quality: Concern —
cd-path extraction is fragile (see Issues) - ✅ Testing: Pass for coverage breadth; gap: no quoted-
cdor chained-cdcases (the two cases that break) - ❌ Security: Fail — a protected-branch commit can bypass the gate (false-negative)
- ✅ Performance: Pass
- ✅ PR Description & Glossary: Pass (good glossary, narrative bullets)
- ✅ Summary Bullet Narrative: Pass
- ✅ Technical Decisions (AgDR): N/A (shell bugfix, no library/architecture decision)
- ✅ Adopter Handbooks: N/A (shell-only diff; no language handbook fires; architecture/general handbooks don't apply)
Issues Found
⛔ BLOCKING — false-negative: quoted cd path lets a protected-branch commit slip through
The commit-side extractor at .claude/hooks/block-main-push.sh:137:
sed -n 's/^[[:space:]]*cd[[:space:]]\{1,\}\([^[:space:];&|][^[:space:];&|]*\).*/\1/p'
captures every char after cd that is not space/;/&/|. A double-quote is NOT excluded, so for:
cd "/path/to/worktree" && git commit -m x
it captures the token including the literal quotes: "/path/to/worktree". The hook then runs git -C '"/path/to/worktree"' branch --show-current, which fails (fatal: cannot change to '"/path...', rc 128, empty stdout). Empty branch → the protected-branch check is skipped → the commit is allowed.
Verified against a sandbox (primary on dev, worktree wtmain on main):
cd "$sb/wtmain" && git commit -m x → got rc=0 (want 2) ← protected-branch commit NOT blocked
cd $sb/wtmain && git commit -m x → got rc=2 ← unquoted works
And the underlying git -C behaviour:
git -C /real/path branch --show-current → 'main' rc 0 (would block)
git -C '"/real/path"' branch --show-current → '' rc 128 (slips through)
Quoting a cd path is completely ordinary shell usage (paths with spaces, or just habit), so this is a realistic bypass in the dangerous direction the fix is meant to close. This must be fixed before merge.
Suggested fix: strip surrounding quotes from the captured token before git -C, e.g. after extraction WORKTREE_PATH="${WORKTREE_PATH%\"}"; WORKTREE_PATH="${WORKTREE_PATH#\"}" (and the single-quote pair), and/or extract via a quote-aware match. Add a quoted-cd-into-protected-worktree case to the test suite.
nit (non-blocking) — chained cd resolves the wrong (first) target
For cd a && cd b && git commit, the sed \([^...]*\) capture stops at the first space, so it always extracts the first cd target, not the final one the commit actually runs in. Observed: cd $sb && cd $sb/wtfeat && git commit (final worktree on a feature branch, primary on dev) → got rc=2 (false block). This is the safe direction (over-blocks rather than under-blocks), so it doesn't gate the merge, but it's a correctness gap worth fixing alongside the quoted-path fix — and arguably a chained cd into a protected worktree last would be a second false-negative path. Resolving the last cd target (not the first) closes both. Consider a test case for it too.
Verdict
CHANGES REQUESTED
The push side is correct; the commit-side cd extraction has a confirmed false-negative (quoted path → protected-branch commit bypasses the gate). Strip the quotes (and ideally resolve the last cd target), add the missing test cases, and re-request review.
🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 656ae4629a3ef0ff6a04f7715499e391fba33e6e
Addresses the #580 Rex review: - block-main-push.sh now strips surrounding quotes from the `cd <path>` target before `git -C`. Without this, `cd "path" && git commit` passed quotes to git -C → error → empty branch → the protected-branch check was skipped and a commit into a protected worktree slipped through (a false-negative in the dangerous direction). - Resolves the LAST `cd` in a chain (cd a && cd b && commit → b), fixing the secondary over-block Rex noted. - 5 new tests: quoted (double/single) cd into protected → blocks; quoted cd into feature → passes; chained cd resolves last target both directions. Suite now 31/31. Refs #549
atlas-apex
left a comment
There was a problem hiding this comment.
Code Review: PR #580 (re-review)
Commit: 99c6652f6c0e6f4994633ec902c5520eaa5b6207
Summary
Follow-up fix for the false-negative caught in my prior review of this PR. The earlier cd <path> && git commit worktree-branch resolution passed the literal cd token with surrounding quotes to git -C, which errored, produced an empty branch, and let a commit into a protected-branch worktree slip past the [ -n "$CURRENT_BRANCH" ] guard. Commit 99c6652 (1) strips surrounding single/double quotes before git -C, and (2) resolves the LAST cd in a compound chain via tail -n 1. New tests added; suite now 31/31.
Verification performed
- Original false-negative is CLOSED. Built an isolated repro (primary checkout on
feature/safe, sibling worktree ondev). A quotedcd "<protected-worktree>" && git commitnow exits 2 (BLOCK). Bare-path cd also blocks; cd into a feature-branch worktree passes (exit 0). - Suite: 31 PASS / 0 FAIL, suite exit code 0. The
fatal: '.../wt' already existsline in the run output is a benign|| true-guarded test-setup artifact in case (d) — it precedes a case that passes and affects no outcome (31 PASS lines, 0 FAIL lines confirmed by grep). - Extraction shell is sound. The grep alternation
("[^"]*"|'[^']*'|[^[:space:];&|]+)tries double-quoted, then single-quoted, then bare;tail -n 1selects the last cd; the sed stripscdprefix then leading/trailing quotes. Verified directly against:- quoted (double + single), bare, and spaces-in-path → correct path extracted;
- chained
cd a && cd bresolvesb, andcd b && cd aresolvesa(last wins, both directions); cd "/a" "/b"extracts/a— which matches bash's realcdsemantics (extra args ignored), so not a divergence;- unbalanced-quote
cd "/proto/wt && git commitextracts/proto/wtcorrectly.
- No NEW false-negative introduced. Compared against
upstream/devbase: the base commit-check resolved ONLYgit branch --show-currentagainst the session cwd. Cases that still resolve to empty under the new code (git -C <path> commitwith nocd,pushd, unexpanded~/$HOME) are pre-existing gaps the base never handled either — the PR strictly adds thecd <path>detection it previously mishandled. No case that the base would block is now allowed through. - Safe-direction regressions hold. Push to protected blocks (explicit ref + refspec dst); push to feature passes when cwd=dev; bare
git pushon dev falls back to local HEAD and blocks; tag pushes pass; plain commit on protected blocks; plain commit on feature passes.
Checklist Results
- Architecture & Design: Pass (reuses
_lib-extract-push-ref.sh, consistent withvalidate-branch-name.sh) - Code Quality: Pass (quote-strip + last-match well-commented; rationale cites this review)
- Testing: Pass (31/31; covers quoted/bare/chained/both-directions + the regression set)
- Security: Pass (the fix IS a security hardening — closes the empty-branch slip-through)
- Performance: Pass (n/a; shell hook)
- PR Description & Glossary: Pass (Glossary present; worktree / session-cwd / push-dst terms defined)
- Summary Bullet Narrative: Pass (what + why)
- Technical Decisions (AgDR): N/A (bug fix to existing hook; no new dependency/architecture/pattern)
- Adopter Handbooks: N/A (no handbooks triggered by this diff)
Issues Found
None.
Suggestions
Non-blocking, for a future pass: the git -C <path> (no cd) and pushd <path> shapes, plus unexpanded ~/$HOME, still resolve to empty → session-cwd fallback. These are pre-existing and out of scope here, but if any of them ever becomes a realistic operator pattern, the same empty-branch-means-skip shape that caused the #580 FN would re-appear for those forms. Worth a tracked follow-up rather than a blocker.
Verdict
APPROVED
🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 99c6652f6c0e6f4994633ec902c5520eaa5b6207
atlas-apex
left a comment
There was a problem hiding this comment.
Code Review: PR #580 (re-review)
Commit: 99c6652f6c0e6f4994633ec902c5520eaa5b6207
Summary
Follow-up fix for the false-negative caught in my prior review of this PR. The earlier cd <path> && git commit worktree-branch resolution passed the literal cd token with surrounding quotes to git -C, which errored, produced an empty branch, and let a commit into a protected-branch worktree slip past the [ -n "$CURRENT_BRANCH" ] guard. Commit 99c6652 (1) strips surrounding single/double quotes before git -C, and (2) resolves the LAST cd in a compound chain via tail -n 1. New tests added; suite now 31/31.
Verification performed
- Original false-negative is CLOSED. Built an isolated repro (primary checkout on
feature/safe, sibling worktree ondev). A quotedcd "<protected-worktree>" && git commitnow exits 2 (BLOCK). Bare-path cd also blocks; cd into a feature-branch worktree passes (exit 0). - Suite: 31 PASS / 0 FAIL, suite exit code 0. The
fatal: '.../wt' already existsline in the run output is a benign|| true-guarded test-setup artifact in case (d) — it precedes a case that passes and affects no outcome (31 PASS lines, 0 FAIL lines confirmed by grep). - Extraction shell is sound. The grep alternation
("[^"]*"|'[^']*'|[^[:space:];&|]+)tries double-quoted, then single-quoted, then bare;tail -n 1selects the last cd; the sed stripscdprefix then leading/trailing quotes. Verified directly against:- quoted (double + single), bare, and spaces-in-path → correct path extracted;
- chained
cd a && cd bresolvesb, andcd b && cd aresolvesa(last wins, both directions); cd "/a" "/b"extracts/a— which matches bash's realcdsemantics (extra args ignored), so not a divergence;- unbalanced-quote
cd "/proto/wt && git commitextracts/proto/wtcorrectly.
- No NEW false-negative introduced. Compared against
upstream/devbase: the base commit-check resolved ONLYgit branch --show-currentagainst the session cwd. Cases that still resolve to empty under the new code (git -C <path> commitwith nocd,pushd, unexpanded~/$HOME) are pre-existing gaps the base never handled either — the PR strictly adds thecd <path>detection it previously mishandled. No case that the base would block is now allowed through. - Safe-direction regressions hold. Push to protected blocks (explicit ref + refspec dst); push to feature passes when cwd=dev; bare
git pushon dev falls back to local HEAD and blocks; tag pushes pass; plain commit on protected blocks; plain commit on feature passes.
Checklist Results
- Architecture & Design: Pass (reuses
_lib-extract-push-ref.sh, consistent withvalidate-branch-name.sh) - Code Quality: Pass (quote-strip + last-match well-commented; rationale cites this review)
- Testing: Pass (31/31; covers quoted/bare/chained/both-directions + the regression set)
- Security: Pass (the fix IS a security hardening — closes the empty-branch slip-through)
- Performance: Pass (n/a; shell hook)
- PR Description & Glossary: Pass (Glossary present; worktree / session-cwd / push-dst terms defined)
- Summary Bullet Narrative: Pass (what + why)
- Technical Decisions (AgDR): N/A (bug fix to existing hook; no new dependency/architecture/pattern)
- Adopter Handbooks: N/A (no handbooks triggered by this diff)
Issues Found
None.
Suggestions
Non-blocking, for a future pass: the git -C <path> (no cd) and pushd <path> shapes, plus unexpanded ~/$HOME, still resolve to empty → session-cwd fallback. These are pre-existing and out of scope here, but if any of them ever becomes a realistic operator pattern, the same empty-branch-means-skip shape that caused the #580 FN would re-appear for those forms. Worth a tracked follow-up rather than a blocker.
Verdict
APPROVED
🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 99c6652f6c0e6f4994633ec902c5520eaa5b6207
The original test left abandoned WT_PROTECTED + wt-main worktree setup from a first approach; the active assertion uses a secondary sandbox (SB2). shellcheck flagged WT_PROTECTED as unused (SC2034), failing CI. Removed the dead lines. Refs #549
atlas-apex
left a comment
There was a problem hiding this comment.
Code Review (re-review): PR #580
Commit: 4e74d57ed2cdde5825389b5c343440d5c5f9113f
Prior approval: 99c6652 — one commit added since (4e74d57).
Summary
This is a re-review of a single incremental commit (4e74d57) on top of the previously-approved 99c6652. The new commit removes dead test scaffolding from .claude/hooks/tests/test_block_main_push.sh — an unused WT_PROTECTED=$(make_worktree ...) assignment plus an abandoned wt-main worktree-setup block that shellcheck flagged as SC2034 (unused variable), which was failing CI. No change to block-main-push.sh itself and no change to any assertion logic.
Verification of the four requested claims
-
Removed lines were genuinely dead — CONFIRMED. The incremental diff removes only the
WT_PROTECTED=...assignment and the abandoned(cd "$SB" && git worktree add ... wt-main ...)block.grep -nE 'WT_PROTECTED|wt-main'against the new file returns nothing. The active "commit into a protected worktree blocks" assertion uses theSB2secondary sandbox (make_sandbox "main"), not the removedWT_PROTECTED/wt-mainscaffolding — that secondary-sandbox approach was already the live path in the prior commit (the comment block above it literally read "Use a simpler approach: create a secondary repo on 'main'"). The removed lines were superseded leftovers from an earlier attempt, never referenced by anyrun_case. -
Suite still passes — CONFIRMED. Ran
bash .claude/hooks/tests/test_block_main_push.sh→PASS: 31 FAIL: 0, exit 0. (PR body says 26; the actual count is 31 after the #580 review follow-up cases were added — the higher number is the suite as-merged, all green.) -
shellcheck clean — CONFIRMED. Ran
shellcheck .claude/hooks/block-main-push.sh .claude/hooks/tests/test_block_main_push.sh→ exit 0, no findings. The SC2034 that was failing CI is gone. -
Security-critical behaviour unchanged — CONFIRMED. The behaviour from the prior approval is intact and still asserted green:
- Quoted-
cdinto a protected worktree BLOCKS:cd "protected" (double-quoted) && commit blocksPASS;cd 'protected' (single-quoted) && commit blocksPASS. Quote-stripping prevents the false-negative slip-through (quotes reachinggit -C→ empty branch → skipped check). - Last-
cdresolution:chained cd: last=feature passesPASS;chained cd: last=protected blocksPASS. - Quote-strip does not over-block legitimate feature work:
cd "feature-wt" (quoted) && commit passesPASS. - All regression cases (push to protected blocks, bare push falls back to local HEAD, tag push passes, plain commit on protected blocks) still PASS.
- Quoted-
Checklist Results
- Architecture & Design: Pass (test-only change; no production-code delta)
- Code Quality: Pass (removes dead code; shellcheck clean)
- Testing: Pass (31/31, behaviour-level assertions intact)
- Security: Pass (protected-worktree block + quoted/chained-cd resolution unchanged and asserted)
- Performance: N/A
- PR Description & Glossary: Pass (Glossary present; summary narrative)
- Technical Decisions (AgDR): N/A (test-scaffolding cleanup, no new decision)
- Adopter Handbooks: N/A (no handbooks loaded — diff is shell test/hook, no language-handbook trigger)
Issues Found
None. The incremental commit is a pure dead-code removal that fixes a CI shellcheck failure with no functional impact. The protected-branch enforcement that the prior approval rested on is unchanged in block-main-push.sh and still proven by the green suite.
Suggestions
None blocking.
Verdict
APPROVED
Reviewed by Rex (Code Reviewer Agent)
Reviewed commit: 4e74d57ed2cdde5825389b5c343440d5c5f9113f
atlas-apex
left a comment
There was a problem hiding this comment.
Code Review (re-review): PR #580
Commit: 4e74d57ed2cdde5825389b5c343440d5c5f9113f
Prior approval: 99c6652 — one commit added since (4e74d57).
Summary
This is a re-review of a single incremental commit (4e74d57) on top of the previously-approved 99c6652. The new commit removes dead test scaffolding from .claude/hooks/tests/test_block_main_push.sh — an unused WT_PROTECTED=$(make_worktree ...) assignment plus an abandoned wt-main worktree-setup block that shellcheck flagged as SC2034 (unused variable), which was failing CI. No change to block-main-push.sh itself and no change to any assertion logic.
Verification of the four requested claims
-
Removed lines were genuinely dead — CONFIRMED. The incremental diff removes only the
WT_PROTECTED=...assignment and the abandoned(cd "$SB" && git worktree add ... wt-main ...)block.grep -nE 'WT_PROTECTED|wt-main'against the new file returns nothing. The active "commit into a protected worktree blocks" assertion uses theSB2secondary sandbox (make_sandbox "main"), not the removedWT_PROTECTED/wt-mainscaffolding — that secondary-sandbox approach was already the live path in the prior commit (the comment block above it literally read "Use a simpler approach: create a secondary repo on 'main'"). The removed lines were superseded leftovers from an earlier attempt, never referenced by anyrun_case. -
Suite still passes — CONFIRMED. Ran
bash .claude/hooks/tests/test_block_main_push.sh→PASS: 31 FAIL: 0, exit 0. (PR body says 26; the actual count is 31 after the #580 review follow-up cases were added — the higher number is the suite as-merged, all green.) -
shellcheck clean — CONFIRMED. Ran
shellcheck .claude/hooks/block-main-push.sh .claude/hooks/tests/test_block_main_push.sh→ exit 0, no findings. The SC2034 that was failing CI is gone. -
Security-critical behaviour unchanged — CONFIRMED. The behaviour from the prior approval is intact and still asserted green:
- Quoted-
cdinto a protected worktree BLOCKS:cd "protected" (double-quoted) && commit blocksPASS;cd 'protected' (single-quoted) && commit blocksPASS. Quote-stripping prevents the false-negative slip-through (quotes reachinggit -C→ empty branch → skipped check). - Last-
cdresolution:chained cd: last=feature passesPASS;chained cd: last=protected blocksPASS. - Quote-strip does not over-block legitimate feature work:
cd "feature-wt" (quoted) && commit passesPASS. - All regression cases (push to protected blocks, bare push falls back to local HEAD, tag push passes, plain commit on protected blocks) still PASS.
- Quoted-
Checklist Results
- Architecture & Design: Pass (test-only change; no production-code delta)
- Code Quality: Pass (removes dead code; shellcheck clean)
- Testing: Pass (31/31, behaviour-level assertions intact)
- Security: Pass (protected-worktree block + quoted/chained-cd resolution unchanged and asserted)
- Performance: N/A
- PR Description & Glossary: Pass (Glossary present; summary narrative)
- Technical Decisions (AgDR): N/A (test-scaffolding cleanup, no new decision)
- Adopter Handbooks: N/A (no handbooks loaded — diff is shell test/hook, no language-handbook trigger)
Issues Found
None. The incremental commit is a pure dead-code removal that fixes a CI shellcheck failure with no functional impact. The protected-branch enforcement that the prior approval rested on is unchanged in block-main-push.sh and still proven by the green suite.
Suggestions
None blocking.
Verdict
APPROVED
Reviewed by Rex (Code Reviewer Agent)
Reviewed commit: 4e74d57ed2cdde5825389b5c343440d5c5f9113f
Summary
git branch --show-currentagainst the hook's cwd (the harness primary checkout). When the primary checkout sat ondevormainbut the actualgit pushtargeted a feature branch, the hook blocked the push incorrectly. Now reuses_lib-extract-push-ref.sh(the same libvalidate-branch-name.shuses for the identical worktree-cwd problem, [Bug] Validation hooks resolve git context from $PWD, not the worktree where gh/git ran (breaks /fan-out) #194/[Bug] validate-branch-name.sh misreads git-push refspecs and redirections as branch names #547) to read the DESTINATION branch directly from the push command — the ground truth regardless of $PWD.cd <path> && git commit— for compound shell commands thatcdinto a different worktree before committing, the hook now resolves the branch of the TARGET worktree viagit -C <path> branch --show-currentinstead of the session cwd. Plaingit commit(nocdprefix) retains the original behaviour, preserving the normal single-worktree case.test_block_main_push.sh) covering all five required scenarios: push to protected still blocks, push to feature branch passes when cwd=dev, tag push passes,cd wt && git commiton a feature-branch worktree passes while cwd=dev, and plain commit on a protected branch still blocks.Testing
bash .claude/hooks/tests/test_block_main_push.sh # Expected: PASS: 26 FAIL: 0To reproduce the original bug manually:
dev(a protected branch).git worktree add ../wt some-feature-branch.echo '{"tool_input":{"command":"cd ../wt && git commit -m test"}}' | bash .claude/hooks/block-main-push.shRefs #549
Glossary
git worktree) that shares one git object store but is checked out to a different branch/directory. The harness fan-out pattern creates one worktree per parallel agent.main,master,dev,develop) that must not receive direct commits or pushes; all changes must go through a PR. Configurable via.claude/project-config.json → .git.protected_branches[].git pushwill write to, extracted from the command's explicit ref orsrc:dstrefspec. Distinct from the source branch, which may beHEADor a local tracking branch._lib-extract-push-ref.shgit pushcommand string, handling flags, refspecs, redirections, and tag pushes. Already used byvalidate-branch-name.shfor the same worktree-cwd reason.