Skip to content

fix(hooks): anchor the short-flag alternative in block-git-force-push-no-lease - #66

Open
lapc506 wants to merge 2 commits into
mainfrom
fix/force-push-rule-unanchored-short-flag
Open

fix(hooks): anchor the short-flag alternative in block-git-force-push-no-lease#66
lapc506 wants to merge 2 commits into
mainfrom
fix/force-push-rule-unanchored-short-flag

Conversation

@lapc506

@lapc506 lapc506 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

The defect

hooks/rules/rules.yaml, rule block-git-force-push-no-lease, positive match:

git[[:space:]]+push.*(-f\b|--force([[:space:]]|$))

The -f\b alternative never required -f to be a standalone argument. \b
constrains only what follows the flag, so -f matched anywhere in the command
string — including inside the bracket expression of an unrelated regex. f is a
word character and ] is not, so the boundary was satisfied and the rule fired
on 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:

git push > "$log" 2>&1; grep -aoE '[0-9a-f]{10}\.\.[0-9a-f]{10}' "$log"

Payload piped into hooks/lib/eval-rule.sh block-git-force-push-no-lease:

Command Before (ac7d35d) After
git push > "$log" 2>&1; grep -aoE '[0-9a-f]{10}\.\.[0-9a-f]{10}' "$log" exit 2 (blocked) exit 0
git push origin main && grep -E '[a-f]+' out.txt exit 2 (blocked) exit 0
git push -f origin feature-x (true positive) exit 2 exit 2

Why this one is worth fixing rather than bypassing

The block message hands the reader git push --force-with-lease. An agent
complying 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

-      pattern: 'git[[:space:]]+push.*(-f\b|--force([[:space:]]|$))'
+      pattern: 'git[[:space:]]+push.*([[:space:]]-f\b|--force([[:space:]]|$))'

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. The git[[:space:]]+push prefix
    guarantees at least one character before the flag, so that branch is
    unreachable.
  • \b is kept after -f rather than tightened to ([[:space:]]|$). That
    tightening would have opened a hole: git push -f; — flag followed by a
    separator — would stop matching. The new test
    blocks-force-short-flag-at-end-of-command guards this.

hooks/rules/rules.json regenerated with npm run build-rules (the CI
sync check in test-hooks.yml verifies it).

Tests

Seven added to the rule's own tests array, in the existing shape. Four are
specifications:

  • allows-plain-push-then-hex-range-grep — the exact reproduction, expects 0
  • allows-plain-push-then-a-f-range-grep[a-f] variant, expects 0
  • blocks-force-short-flag-at-end-of-commandgit push -f, expects 2
  • allows-other-command-f-flag-before-the-push — expects 0

Three 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:

Cross-cutting results: 23 / 23 passed
Results: 353 / 353 passed

Baseline on ac7d35d measured the same way: 346 / 346 passed. All six
pre-existing tests for this rule still pass.


Known limitation 1 — a sibling command's genuine -f still fires

[[:space:]]-f\b requires whitespace before the flag, which is exactly what the
bracket expression lacked. Another command's real -f flag has that whitespace
too, so a compound command still false-positives. Measured on this branch:

Command Exit Fires?
git push origin main && grep -f patterns.txt build.log 2 yes — false positive
git push origin main; tar -xz -f a.tgz 2 yes — false positive
git push origin main && rm -f stale.lock 2 yes — false positive
rm -f stale.lock && git push origin main 0 no — .* only looks forward

The obvious cure was measured and rejected

Narrowing .* to [^|;&]* — the shape block-no-verify already uses in this
same 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 mainrm -f stale.lock) does not fire, on either
pattern — . 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 tests array — as JSON, it
is data.

Worth considering separately: the same distinction was added to dojo-os's
secrets guard by evaluating the pattern in a command mode, where a quoted
string 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_pattern is evaluated over the whole command

not_pattern: '--force-with-lease' is tested against the entire command string
rather than the push segment. Measured:

git push --force-with-lease origin a && git push --force origin b

exit 0. The second push is bare --force and passes, because a different
push elsewhere in the string carries the lease. Same root cause as limitation 1
— the rule reasons about the whole command string, not the git push segment.

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-lease was the only one matching the literal
-x\b form, but three others match a short flag as a bare substring:

Rule Flag Probe Result
inline-db-mutation-psql -c in (-c|--command) psql -f up.sql && echo done-c CREATE exit 2 (block)
inline-db-mutation-mysql -e in (-e|--execute) mysql -h host && echo done-e DELETE exit 2 (block)
warn-curl-mutating-supabase-rest -X, -d curl 'https://abc.supabase.co/rest/v1/t' --header-X POST action=warn fires (exit 0)

Left unfixed deliberately — the change is not the identical one-token edit:

  • The short flag sits inside an alternation group shared with a long flag
    ((-c|--command)), so the anchor has to go before the whole group, which also
    changes how --command / --execute match. In the curl rule the flags appear
    at four points across a positional, order-sensitive alternation.
  • For the two DB rules the anchor would not be the dominant fix anyway. Their
    [^|]* / [^|<]* spans && and ; into unrelated commands, so a correctly
    anchored flag still false-positives. Measured:
    psql -f up.sql && grep -c CREATE up.sqlexit 2, blocking a read-only
    grep. 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

…-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

@dojo-code-reviewer dojo-code-reviewer Bot 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.

✅ 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

@dojo-code-reviewer dojo-code-reviewer Bot 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.

✅ Approved

Approved — 0 blockers, 2 P3. Confidence: 4.60/5.00.

Walkthrough

⚠️ Governance Warning: This PR targets the 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 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]

  • hooks/rules/rules.yaml:1399 — 🔵 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]


Total findings: 2 business context (2 total)

Comment thread hooks/rules/rules.json
{
"field": "command",
"pattern": "git[[:space:]]+push.*(-f\\b|--force([[:space:]]|$))"
"pattern": "git[[:space:]]+push.*([[:space:]]-f\\b|--force([[:space:]]|$))"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 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]

Comment thread hooks/rules/rules.yaml
# - `--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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 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]

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.

1 participant