Repository navigation
fix(hooks): the pre-commit gate was failing open, twice - #7
Merged
Merged
Conversation
pluginslab
force-pushed
the
ci/quality-gate
branch
from
September 21, 2026 16:35
bec1ed1 to
8478970
Compare
pluginslab
force-pushed
the
fix/pre-commit-fail-open
branch
from
September 21, 2026 16:36
ba4d785 to
b297600
Compare
Two independent ways for this gate to do nothing, both silent.
1. It only matched `git commit` as a prefix.
case "$command" in
git\ commit*) ;;
*) exit 0 ;;
esac
So `git add -A && git commit -m x` -- the idiom most people and most
agents actually type -- never triggered it. Neither did `cd sub && git
commit`, nor `git -C dir commit`. Verified against the real hook: of four
common commit shapes, exactly one fired the gate.
Every commit in the session that produced this fix used the `git add &&
git commit` form, so none of them were gated. quality.sh was run by hand
each time, which is the only reason that is a footnote and not an
incident.
Now matches `git ... commit` anywhere in the command, allowing flags and
paths between the two words but not a command separator. The tradeoff is
that `git log --grep=commit` will now run the suite; that direction is
the safe one, and a block names the command that triggered it so a false
positive explains itself.
2. It parsed the payload with python3 and never checked python3 existed.
On a host without it, the capture came back empty, matched nothing, and
the hook exited 0. No gate, no warning. It now tries python3, falls back
to jq, and if neither is present prints "THE QUALITY GATE IS NOT RUNNING"
on stderr.
It still does not block in that case: this hook fires before every Bash
call, so blocking on its own internal errors would wedge the session.
Failing open is the right call. Failing open *silently* was not.
Tests grew from 5 cases to 20, including the four commit shapes that used
to bypass the gate and the no-parser contract. The gate-firing cases are
now guarded on a parser being available, so the suite stays honest on a
host that legitimately cannot gate rather than going red for behaving as
designed.
Verified: macOS 92 passed, Linux+python3 92 passed, Linux without any
JSON parser 78 passed with 14 correctly skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pluginslab
force-pushed
the
fix/pre-commit-fail-open
branch
from
September 21, 2026 16:38
b297600 to
1898987
Compare
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.
Stacked on #5 (which is stacked on #6). Chain:
main← #6 ← #5 ← this.Two independent ways for the quality gate to do nothing, both silent.
1. It only matched
git commitas a prefixMeasured against the real hook, with a
quality.shrigged to fail:git commit -m xgit add -A && git commit -m xcd foo && git commit -m xgit -C . commit -m xThe first row is the only shape that worked. The second is the idiom most people — and most agents — actually type.
Worth stating plainly: every commit in the session that produced this fix used
git add … && git commit, so none of them were gated.quality.shwas run by hand before each one, which is the only reason that's a footnote rather than an incident. A gate you have to remember to run manually is not a gate; it's a convention with extra steps.Now matches
git … commitanywhere in the command, allowing flags and paths between the two words but not a command separator, sogit status && make commit-docsstill passes through.Tradeoff, deliberately taken:
git log --grep=commitwill now run the suite. A needless quality run costs seconds; a skipped gate costs a broken commit. To keep a false positive from being baffling, a block now printsTriggered by: <command>.2. It parsed the payload with
python3and never checkedpython3existedOn a host without it, the capture came back empty, matched no pattern, and the hook exited 0. No gate, no warning, no way to notice. This was flagged in #6 and deferred to here.
Now: try
python3, fall back tojq, and if neither is available printIt still does not block in that case, and that's on purpose — this hook fires before every Bash call, so blocking on its own internal errors would wedge the session. Failing open is correct here. Failing open silently was not. Same reasoning for an empty payload, which stays quiet since no real invocation looks like that and warning would cry wolf on every command.
Tests: 5 cases → 20
Including all four commit shapes that used to bypass the gate, four that must still pass through, the no-parser contract, and the empty-payload case.
The gate-firing assertions are now guarded on a parser actually being available. Without that guard the suite goes red on a minimal host for behaving exactly as designed — which is the same "the test encodes the author's machine" mistake that hid the Linux mtime bug in #6. When skipped, it says so.
ubuntu-latest)Verified in
debian:bookworm-slimvia Docker, not by pushing and hoping.Note
This PR's own hook caught a syntax error in itself mid-development — an inline
[[ =~ ]]pattern containing;and&is parsed as shell syntax before the regex engine sees it. The pattern now lives in a variable, which is the correct idiom. Small thing, but a decent sign the gate works once it actually fires.🤖 Generated with Claude Code