Skip to content

fix(hooks): the pre-commit gate was failing open, twice - #7

Merged
pluginslab merged 1 commit into
mainfrom
fix/pre-commit-fail-open
Sep 21, 2026
Merged

pluginslab merged 1 commit into
mainfrom
fix/pre-commit-fail-open

Conversation

@pluginslab

Copy link
Copy Markdown
Owner

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 commit as a prefix

case "$command" in
  git\ commit*) ;;
  *) exit 0 ;;
esac

Measured against the real hook, with a quality.sh rigged to fail:

Command Gate ran?
git commit -m x YES
git add -A && git commit -m x no
cd foo && git commit -m x no
git -C . commit -m x no

The 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.sh was 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 … commit anywhere in the command, allowing flags and paths between the two words but not a command separator, so git status && make commit-docs still passes through.

Tradeoff, deliberately taken: git log --grep=commit will 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 prints Triggered by: <command>.

2. It parsed the payload with python3 and never checked python3 existed

On 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 to jq, and if neither is available print

pre-commit: cannot read the hook payload — no working python3 or jq on PATH.
pre-commit: THE QUALITY GATE IS NOT RUNNING. Install python3 or jq to re-enable it.

It 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.

Environment Result
macOS 92 passed
Linux + python3 (= ubuntu-latest) 92 passed
Linux, no python3 or jq 78 passed, 14 correctly skipped

Verified in debian:bookworm-slim via 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

@pluginslab
pluginslab force-pushed the fix/pre-commit-fail-open branch from ba4d785 to b297600 Compare September 21, 2026 16:36
@pluginslab
pluginslab changed the base branch from ci/quality-gate to main September 21, 2026 16:37
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
pluginslab force-pushed the fix/pre-commit-fail-open branch from b297600 to 1898987 Compare September 21, 2026 16:38
@pluginslab
pluginslab merged commit 97a63cf into main Sep 21, 2026
1 check passed
@pluginslab
pluginslab deleted the fix/pre-commit-fail-open branch September 21, 2026 16:39
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