fix(hooks): anchor the short-flag alternative in block-git-force-push-no-lease - #66
fix(hooks): anchor the short-flag alternative in block-git-force-push-no-lease#66lapc506 wants to merge 2 commits into
Conversation
…-no-lease
The positive match was `git[[:space:]]+push.*(-f\b|--force([[:space:]]|$))`.
The `-f\b` alternative never required `-f` to be a standalone argument, so it
matched `-f` anywhere in the command string — including inside the bracket
expression of an unrelated regex. A plain `git push` followed by
`grep -aoE '[0-9a-f]{10}'` therefore fired the force-push block on a command
carrying no force flag at all: `f` is a word char and `]` is not, so `\b` was
satisfied.
Requiring a preceding whitespace character makes the flag match only as an
argument. `\b` is kept AFTER the flag rather than tightened to
`([[:space:]]|$)`, so a flag followed by a separator still blocks.
Why this is worth a fix rather than a bypass: the block message hands the
reader `--force-with-lease`, so an agent complying with the false positive
would ADD a force flag to a push that needed none.
Three regression tests added to the rule's own `tests` array — the exact
reproduction, an `[a-f]` variant, and a trailing-flag true positive that
guards the retained `\b`. rules.json regenerated with `npm run build-rules`.
Measured: hooks/test-hooks.sh 349/349 pass, up from 346/346 on ac7d35d.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0191NZw1kufQfsAVPocTH6ju
There was a problem hiding this comment.
✅ Approved
Approved — no findings. Confidence: 5.00/5.00.
Walkthrough
⚠️ Governance Warning
This pull request targets the main branch directly. The project's GitFlow governance model expects feature branches to target develop first (i.e., feature → develop → main). This warning is non-blocking and for informational purposes only; the mergeability verdict remains based purely on code correctness.
Walkthrough & Review Area
This PR resolves a false positive in the block-git-force-push-no-lease hook rule. Previously, the short-flag pattern (-f\b|...) was unanchored at its leading boundary, causing it to trigger on unrelated bracket expressions (e.g., [0-9a-f]{10}) containing a trailing f followed by a non-word character (like ]). This PR anchors the -f flag by requiring a preceding whitespace character ([[:space:]]).
We reviewed the rules manifest changes in hooks/rules/rules.yaml and hooks/rules/rules.json, along with the added regression tests and the evaluation runner in hooks/lib/eval-rule.sh.
Safety Rationale
The change is exceptionally safe as it eliminates a high-noise false positive without reducing the rule's security guard integrity (all true-positive force push forms remain blocked, and the suffix word boundary \b is preserved to match flags followed by statement terminators).
Approved — no findings.
…ord why the obvious cure is rejected The anchor fix requires whitespace before the short flag, which is what the bracket expression lacked. Another command's genuine flag has that whitespace too, so a sibling command in the same compound command still fires the rule. Measured on this branch: git push origin main && grep -f patterns.txt build.log -> exit 2 git push origin main; tar -xz -f a.tgz -> exit 2 git push origin main && rm -f stale.lock -> exit 2 rm -f stale.lock && git push origin main -> exit 0 The last one passes because `.*` only looks forward from `git push`; it is locked in as a true negative. The pattern is deliberately left alone. Narrowing `.*` to `[^|;&]*` — the shape `block-no-verify` already uses — was built on a copy of the hooks tree and measured against both directions. It silences all three false positives and keeps every existing test green, but it also stops blocking two REAL force pushes, because a separator character can legitimately sit between the push and the flag: git push $(git remote | head -1) -f main -> 2 shipped, 0 narrowed git push origin 'feat&fix' -f -> 2 shipped, 0 narrowed Trading a false positive for a missed force push is the wrong direction for this rule, so the three cases are characterized at their current exit code with a comment stating they are NOT desired behaviour and must flip to 0 when the segmentation defect is fixed properly. Also records why these had to be tested as JSON fixtures: the rule reads the outer Bash command, so a probe carrying these strings as arguments blocks itself. The rule has no mention-versus-execution distinction. Measured: hooks/test-hooks.sh 353/353 pass (was 349/349). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0191NZw1kufQfsAVPocTH6ju
There was a problem hiding this comment.
✅ Approved
Approved — 0 blockers, 2 P3. Confidence: 4.60/5.00.
Walkthrough
main branch directly. Standard GitFlow conventions for this repository expect feature branches to target develop before being merged into main.
Review Walkthrough
This PR fixes a false positive in the block-git-force-push-no-lease rule where an unanchored -f\b matched substrings (e.g. [a-f] inside character classes of subsequent grep commands) by requiring a preceding whitespace character ([[:space:]]-f\b). It also adds comprehensive unit tests to verify this fix and documents known limitations with rule segmentation.
Reviewed Files & Areas
hooks/rules/rules.yaml: Rule definition and regex patterns for the git force-push blocker.hooks/rules/rules.json: Generated runtime JSON configuration and tests.
Safety Rationale
The proposed pattern correctly narrows the match surface to avoid false-positive triggers without creating evasion holes, and all existing and new test cases pass successfully.
Approved — 0 blockers, 2 P3.
🔵 P3 — Minor
hooks/rules/rules.json:1664— 🔵 P3 (minor) — Mirroring the change torules.yaml, the--forcealternative in the JSON pattern should also be anchored with[[:space:]]to prevent false positives on commands containing--no-force.
[pass 1]
hooks/rules/rules.yaml:1399— 🔵 P3 (minor) — The--forcealternative currently lacks a preceding whitespace anchor. This means a command ending in--no-force(such asgit push --no-force) matches--force([[:space:]]|$)and will be falsely blocked, even though it is a valid option to disable force pushing. We can fix this by adding[[:space:]]before--forceas well.
[pass 1]
Total findings: 2 business context (2 total)
| { | ||
| "field": "command", | ||
| "pattern": "git[[:space:]]+push.*(-f\\b|--force([[:space:]]|$))" | ||
| "pattern": "git[[:space:]]+push.*([[:space:]]-f\\b|--force([[:space:]]|$))" |
There was a problem hiding this comment.
🔵 P3 (minor) — Mirroring the change to rules.yaml, the --force alternative in the JSON pattern should also be anchored with [[:space:]] to prevent false positives on commands containing --no-force.
[pass 1]
| # - `--force` must be followed by whitespace or end-of-string so we | ||
| # don't accidentally match --force-with-lease, which has a `-` after | ||
| # `--force`. | ||
| - field: command |
There was a problem hiding this comment.
🔵 P3 (minor) — The --force alternative currently lacks a preceding whitespace anchor. This means a command ending in --no-force (such as git push --no-force) matches --force([[:space:]]|$) and will be falsely blocked, even though it is a valid option to disable force pushing. We can fix this by adding [[:space:]] before --force as well.
[pass 1]
The defect
hooks/rules/rules.yaml, ruleblock-git-force-push-no-lease, positive match:The
-f\balternative never required-fto be a standalone argument.\bconstrains only what follows the flag, so
-fmatched anywhere in the commandstring — including inside the bracket expression of an unrelated regex.
fis aword character and
]is not, so the boundary was satisfied and the rule firedon a command carrying no force flag at all.
Reproduction, measured
The blocked command was a plain push whose log was then scanned for a
commit-range:
Payload piped into
hooks/lib/eval-rule.sh block-git-force-push-no-lease:ac7d35d)git push > "$log" 2>&1; grep -aoE '[0-9a-f]{10}\.\.[0-9a-f]{10}' "$log"git push origin main && grep -E '[a-f]+' out.txtgit push -f origin feature-x(true positive)Why this one is worth fixing rather than bypassing
The block message hands the reader
git push --force-with-lease. An agentcomplying with this false positive would therefore add a force flag to a push
that needed none — a force-push guard whose false positive causes a force push.
The fix
One token: a required preceding whitespace character. Notes on what was
deliberately not changed, both recorded in the rule's comment block:
^is not added as a second alternative. Thegit[[:space:]]+pushprefixguarantees at least one character before the flag, so that branch is
unreachable.
\bis kept after-frather than tightened to([[:space:]]|$). Thattightening would have opened a hole:
git push -f;— flag followed by aseparator — would stop matching. The new test
blocks-force-short-flag-at-end-of-commandguards this.hooks/rules/rules.jsonregenerated withnpm run build-rules(the CIsync check in
test-hooks.ymlverifies it).Tests
Seven added to the rule's own
testsarray, in the existing shape. Four arespecifications:
allows-plain-push-then-hex-range-grep— the exact reproduction, expects 0allows-plain-push-then-a-f-range-grep—[a-f]variant, expects 0blocks-force-short-flag-at-end-of-command—git push -f, expects 2allows-other-command-f-flag-before-the-push— expects 0Three are characterization tests for known limitation 1 below — they expect
the current exit code, not the desired one, and carry a comment block saying so.
bash hooks/test-hooks.sh:Baseline on
ac7d35dmeasured the same way: 346 / 346 passed. All sixpre-existing tests for this rule still pass.
Known limitation 1 — a sibling command's genuine
-fstill fires[[:space:]]-f\brequires whitespace before the flag, which is exactly what thebracket expression lacked. Another command's real
-fflag has that whitespacetoo, so a compound command still false-positives. Measured on this branch:
git push origin main && grep -f patterns.txt build.loggit push origin main; tar -xz -f a.tgzgit push origin main && rm -f stale.lockrm -f stale.lock && git push origin main.*only looks forwardThe obvious cure was measured and rejected
Narrowing
.*to[^|;&]*— the shapeblock-no-verifyalready uses in thissame file — was built on a copy of the hooks tree and probed in both directions.
It silences all three false positives and keeps every existing test green. It
also stops blocking two real force pushes, because a separator character can
legitimately sit between the push and the flag:
| Real force push | Shipped | Narrowed to
[^|;&]*|| -- | -- | -- |
|
git push $(git remote \| head -1) -f main| 2 | 0 — missed ||
git push origin 'feat&fix' -f| 2 | 0 — missed ||
git push origin main --force && echo ok| 2 | 2 |Trading a false positive for a missed force push is the wrong direction for this
rule, so the pattern is left alone. A correct fix needs shell-aware segment
splitting, not a tighter character class, and belongs in its own PR.
Also measured: a newline-separated compound command
(
git push origin main⏎rm -f stale.lock) does not fire, on eitherpattern —
.does not cross a newline here.Known limitation 2 — this rule cannot be probed from the tool it guards
The rule matches on the outer Bash command string, so a probe that carries these
candidate commands as arguments — building the hook's JSON payload from a
heredoc, for instance — is read by the rule and blocked. There is no
mention-versus-execution distinction: a quoted string is treated as a command.
The workaround used throughout this PR is to write each payload to a file with a
non-Bash tool and pipe the file in, so the fixture never appears on a command
line. That is also why every case above lives in the
testsarray — as JSON, itis data.
Worth considering separately: the same distinction was added to dojo-os's
secrets guard by evaluating the pattern in a
commandmode, where a quotedstring is data unless an executor (
eval,bash -c,$(…),xargs) runs it.Without something equivalent, agents cannot test this rule from Bash, and the
harness is the only route.
Known limitation 3 —
not_patternis evaluated over the whole commandnot_pattern: '--force-with-lease'is tested against the entire command stringrather than the push segment. Measured:
→ exit 0. The second push is bare
--forceand passes, because a differentpush elsewhere in the string carries the lease. Same root cause as limitation 1
— the rule reasons about the whole command string, not the
git pushsegment.Finding — three sibling rules carry the same unanchored short-flag shape
Surveyed all 41 rules for a single-dash short flag with no preceding-whitespace
anchor.
block-git-force-push-no-leasewas the only one matching the literal-x\bform, but three others match a short flag as a bare substring:inline-db-mutation-psql-cin(-c|--command)psql -f up.sql && echo done-c CREATEinline-db-mutation-mysql-ein(-e|--execute)mysql -h host && echo done-e DELETEwarn-curl-mutating-supabase-rest-X,-dcurl 'https://abc.supabase.co/rest/v1/t' --header-X POSTaction=warnfires (exit 0)Left unfixed deliberately — the change is not the identical one-token edit:
(
(-c|--command)), so the anchor has to go before the whole group, which alsochanges how
--command/--executematch. In the curl rule the flags appearat four points across a positional, order-sensitive alternation.
[^|]*/[^|<]*spans&∧into unrelated commands, so a correctlyanchored flag still false-positives. Measured:
psql -f up.sql && grep -c CREATE up.sql→ exit 2, blocking a read-onlygrep. That is limitation 1's root cause again, in two more rules.Each is worth its own PR with its own regression tests.
Created by Claude Code on behalf of @lapc506