There is a line in your AGENTS.md that looks like this:
## Before committingAlways run `pnpm test` and `pnpm typecheck`.Do not commit if either one fails.You already know what the standard advice says about it. A rule is context, and context competes for attention; a hook is control, and control does not. Move the two commands into a pre-commit hook, the advice goes, and you have turned should into cannot.
That advice is right about the categories and silent about the arithmetic. So here is the arithmetic, run on one repo and one sprint, re-measured at every layer of enforcement. Three results carry the piece:
- A rule your agent honors on 95% of commits still has a 92.31% chance of lapsing at least once across a 50-commit sprint. A per-commit probability compounds, and a sprint is long enough to compound a 5% skip rate into near-certainty.
- The obvious fix - a git pre-commit hook plus the standard harness-side script that blocks the
--no-verifyflag - only takes that down to 22.37%. The hook most teams actually write recognizes one way around the gate. There are at least three. - The leak reaches zero at exactly one layer: a gate that runs where no local trick can reach it.
The tension in one sentence: soft compliance is a probability, a probability repeated becomes a certainty of failing somewhere, and the hook was supposed to end that sentence - but it only ends it at the boundary the hook was built to cover.
Every number below follows from declared assumptions about that one repo. You can check the first layer with a pencil.
Check whether you already have this
Section titled “Check whether you already have this”Before the arithmetic, thirty seconds on your own repo settles whether this piece is about you.
Open your AGENTS.md or CLAUDE.md and find every line that tells the agent what to do before committing. grep -n "before committing" AGENTS.md finds most of them.
Then check what stands behind each line. Is the same command wired into a hook that git actually runs - .husky/, lefthook.yml, .pre-commit-config.yaml, or .git/hooks/pre-commit?
If a rule in that file only asks, with no hook behind it, you have this problem, and the numbers below are your numbers. If a hook already enforces it, one more question decides everything, and it is the one this piece adds to the standard advice: can anything disable that hook without editing the repo - an environment variable, a flag, a commit that never touches local git? If nothing can, and your remote branch already requires the checks to pass before a merge, you have built the last layer of this piece. You can stop reading.
A rule that only asks is a bet on attention, and whether you are still making that bet is a thirty-second check.
One sprint, one rule, fifty commits
Section titled “One sprint, one rule, fifty commits”Fix one example and keep it for the rest of the piece. One team’s payments repo: TypeScript, pnpm, a test suite that takes about forty seconds. One coding agent works a two-week sprint on it and lands 50 commits. The repo’s AGENTS.md carries the rule from the intro, word for word.
Say the agent honors the rule on 95% of commits. That number is an assumption, declared here, and it is generous - the rule is one line of prose competing with the task, the diff, the error from three tool calls ago, and a user who wants the feature today. Five commits in a hundred skip the gate. On any single commit that looks fine. Ninety-five percent compliance is a good student.
The problem is the 50 of them. Call the villain by name, because every section below attacks it: the compounding leak, a small chance of failure on each attempt, independent of the last one, which multiplies across attempts until failure somewhere becomes near-certain. A skipped gate is an event you absorb and move on from. A leak rate is a draw from a distribution, and a sprint takes 50 draws.
The next section computes what 50 draws do to a 95% rule. First it computes what 5 of them do, because 5 you can check by hand.
A soft rule’s compliance is a per-commit probability, and a sprint asks the question 50 times.
Five commits on paper
Section titled “Five commits on paper”Shrink the sprint to five commits and do the accounting the obvious way, with no formula in sight. The chance the first commit honors the gate is 0.95. The chance the first two both do is 0.95 x 0.95. Keep multiplying:
0.95 x 0.95 = 0.90250.9025 x 0.95 = 0.8573750.857375 x 0.95 = 0.814506250.81450625 x 0.95 = 0.7737809375The chance all five commits honor the gate is 0.7737809375. The chance the gate lapses at least once is 1 - 0.7737809375 = 0.2262190625.
22.62%. Hold that number. By the fifth commit - before the sprint reaches double digits - there is already a 22.62% chance, better than one in five, that a broken commit has landed in history wearing the team’s own commit message. Nothing dramatic happened in any of those five commits. The leak found them anyway.
Now the closed form. The chance all N commits honor the gate is (1 - 0.05)^N, and at N = 5 that is (0.95)^5 = 0.7737809375. Identical to the hand multiplication, every digit, nothing rounded - the exponent is those five multiplications written shorter. That match is the trust mechanism. It is what lets you use the closed form at a size you will never multiply out by hand:
(0.95)^50 = 0.07691 - 0.0769 = 0.923192.31% chance the rule lapses at least once in a 50-commit sprint. Near certainty, out of a rule that works 95% of the time. That is the first number from the intro, and it arrived by derivation: the same mechanism that produced 22.62%, run out to 50.
The ledger, in one line: the 95% is an assumption; 0.7737809375, 22.62%, and 92.31% are exact arithmetic on it. Nothing in between was estimated.
Five commits in, a 95% rule has already lapsed once with probability 22.62%; fifty commits in, with probability 92.31%.
The hook turns should into cannot
Section titled “The hook turns should into cannot”Now the standard advice, which is correct as far as it goes. Move the two commands out of prose and into a hook that git runs on every commit attempt:
# .husky/pre-commit - runs on every git commit attemptpnpm typecheck && pnpm testHusky (usually paired with lint-staged), Lefthook, and Python’s pre-commit framework are interchangeable here. The runner is illustrative; the argument is about where the boundary sits, and every runner puts it in the same place.
If typecheck fails, the commit aborts. The exit code does the enforcement: no attention required, no memory involved, nothing to honor or forget. And the part that makes hooks better suited to agents than to people is what happens when the gate fires. Its stderr is a repair instruction:
src/api/user.ts:42:18 - error TS2345: Argument of type 'string'is not assignable to parameter of type 'number'.File, line, the type that is wrong, the type that was expected. The agent reads the failure, fixes user.ts:42, and retries the commit with no human in the loop. A person experiences that loop as a tax on patience, and boredom is what sends a person hunting for the escape hatch. An agent does not get bored. What it does instead - reason its way around an obstacle - is the subject of the next section. (The general pattern - a deterministic gate that reads the agent’s output and hands back a repair - is its own post; this piece is about the commit boundary and what stacks above it.)
The team payoff is the quiet one. The hook is checked into the repo as configuration, so every contributor’s agent - any model, any prompt, any private idea of done - faces the same gate. Keep the rule too. It tells the agent why the gate exists, and an agent that knows why repairs faster than one that is merely stopped.
Where the rule asked politely, the hook refuses mechanically, and refusal is what makes the guarantee a different category of thing.
The wall has a door in it
Section titled “The wall has a door in it”Here is the honest counter-argument, and it is a real one. A git hook is a wall with a labeled door, and the label is git commit --no-verify. Every model trained on the public corpus knows the flag exists. Block a commit twice and a helpful agent can reason its way to “the hook is in the way, let me skip it” - the same reflex that wore the flag smooth on human keyboards.
So the standard advice has a second act: close the door inside the agent’s own loop. Claude Code’s hooks include a PreToolUse event that fires before the agent runs any shell command, and exit code 2 denies the command with your message fed back to the agent as feedback:
#!/usr/bin/env bash# .claude/hooks/block-no-verify.sh - wired to PreToolUse on Bashcmd=$(jq -r '.tool_input.command')if echo "$cmd" | grep -qE -- '--no-verify|-n\b'; then echo "Blocked: commit gates are mandatory. Fix the failure, don't skip it." >&2 exit 2fiTwo gates, two jobs. The git hook is the team contract, shipped with every clone. The harness hook is the agent contract, removing the one move the agent might use to escape the first gate. This is where the standard advice ends, and it ends on a word like solved.
Look at the grep before you accept that word. It matches one pattern: the literal --no-verify flag (and bare -n). It checks that a string appears in a command. Everything else an agent might do is invisible to it.
Two gates read as solved. Whether they are depends on a question nobody has asked yet: how many ways around the gate exist?
Re-measure the hook: it is still a leak rate
Section titled “Re-measure the hook: it is still a leak rate”Run the same arithmetic that sank the soft rule, now against the two-gate setup. Three more inputs. These are pure toy numbers, invented so the arithmetic traces - the two filed issues in the honesty section show that the routes exist; no source says how often anything uses them.
- On 20% of commits, the agent’s first attempt trips the hook on a real failure: a test or a type error. Across the 50-commit sprint that is 10 blocked commits.
- Each time a commit is blocked, there is a 5% chance the agent reaches for an escape hatch instead of fixing the failure.
- The blocker script catches an attempt made through the flag. Assume half the attempts go another route instead: an environment variable that disables the hook manager (
HUSKY=0,LEFTHOOK=0,SKIP=), or a commit made through a tool that calls the Git host’s API directly and never touches local git at all.
Per blocked commit, then, the chance a bypass actually lands is 0.05 x 0.5 = 0.025. You have seen this shape already: a per-event leak rate, a number of trials, the compounding leak again - this time at rate 0.025 over 10 trials.
1 - (1 - 0.025)^10 = 1 - 0.975^10 = 0.223722.37% chance that at least one bypassed commit reaches history in the same 50-commit sprint. That is the second number from the intro.
Now the line that earns this section. 22.37% at fifty commits is almost exactly the soft rule’s 22.62% at five. The two-gate setup bought a factor of ten in N, and the leak survived it. Your sprint effectively got ten times longer, is all. And the shape holds at any size: the hook’s column in the table below climbs more slowly than the soft rule’s, but at any N above zero it sits above zero, because its leftover leak is a rate and rates compound.
A hook that recognizes some ways around the gate is a smaller leak rate, and every leak rate compounds.
Name the trick: zero the leak
Section titled “Name the trick: zero the leak”At the moment the 22.37% lands, the goal changes. There are two ways to respond to a leak, and only one of them ends this argument.
Shrinking the leak is the response you have now watched twice. Make the rate smaller: a clearer rule, a stronger model, a blocker that catches one more flag. Every shrink is real, every shrink compounds more slowly, and every one of them still leaves a curve that climbs with N. Double the sprint and you double your exposure to whatever rate is left.
Zeroing the leak is a different category of move: build the gate so there is no rate to compound. Not a very small rate - no rate. The layer becomes incapable of passing a bypassed commit, and N drops out of the arithmetic entirely.
Zeroing takes two pieces, because the routes come in two kinds. The local routes - the flag, the environment variables - can be closed locally: teach the blocker every known disable switch (HUSKY=0, LEFTHOOK=0, OVERCOMMIT_DISABLE=1, SKIP=), and the leak from those routes is gone. The non-local route - a commit that never touches your git - cannot be closed by anything that runs inside your repo, by definition. For that you need a gate that runs where the commit lands anyway.
Stop tuning the leak rate. Build the layer where there is nothing left to tune.
The layer that runs without your git
Section titled “The layer that runs without your git”That layer exists, and you already use the platform that provides it. A required check - a status check the remote will not merge a branch without, or the same rule as a server-side pre-receive hook - evaluates the branch on the host, from the commits exactly as they were pushed, independent of how they were made. --no-verify skips your local hooks. HUSKY=0 skips your local hook manager. An API-made commit skips local git entirely. None of them change what the remote evaluates, because none of them run on the remote.
That is why the third column of the table below is flat at zero, and it is worth being precise about what the zero means. No local action exists that produces a merged commit the gate did not evaluate, so the chance of one is not a function of N at all. The regress - one gate, then a bypass, then another gate behind that one, forever - terminates here, with one honest given, stated plainly: this holds as long as the branch protection itself is not switched off by someone with admin rights. That is a governance problem, not an arithmetic one, and no better hook solves it.
Why keep any local gate once the remote check exists? Timing. The required check runs after the push, on the host, when the work has already left the agent’s hands. The local hook fails the commit in seconds, while the agent still has the file open, and that is what keeps the repair loop cheap.
The full stack, then: the broad local hook (fast feedback, the repair loop, every known local route closed) plus the required remote check (the floor that nothing local can move). Keep the rule for the same reason as before - it explains the gate, and understanding repairs faster than being stopped.
Zero means the failure chance stops being a function of how many commits you make.
The table, run out
Section titled “The table, run out”Same repo, same sprint, one quantity: the chance that at least one broken or bypassed commit reaches history.
| N commits | Soft rule only | Narrow local hook | Broad hook + required remote check |
|---|---|---|---|
| 5 | 22.62% | 2.50% | 0.00% |
| 10 | 40.13% | 4.94% | 0.00% |
| 20 | 64.15% | 9.63% | 0.00% |
| 50 | 92.31% | 22.37% | 0.00% |
Read it one column at a time.
Column one is a countdown. The 22.62% you computed by hand is 40.13% at ten commits and 92.31% at fifty. Add commits, get certainty. Nothing about the rule changed on the way.
Column two is column one stretched. The two-gate setup pushes the curve right by a factor of ten: 22.37% at fifty commits, where the soft rule hit 22.62% at five. It helps, a lot, at every size. It reaches zero at no size, because its leftover leak is a rate and any rate compounds.
Column three does not climb. That flatness is the whole difference between shrinking a leak and zeroing it, and it is the only column where a longer sprint costs you nothing.
Row 50 reconciles both numbers from the intro, and both were derived on the way here: 92.31% by the closed form you verified by hand at N = 5, and 22.37% by the same compounding applied to the hook’s leftover rate.
Two curves and a floor. Only the floor ignores how long the sprint is.
What this doesn’t solve
Section titled “What this doesn’t solve”The blocker script in this piece ships with a documented gap. The two issues are real and filed: anthropics/claude-code#40117, “Agent bypasses git pre-commit hooks using —no-verify, stash, and quiet flags despite explicit deny rules,” and cline/cline#8526, “Add block-no-verify to prevent AI agents from bypassing git hooks” (both verified 2026-08-16). Read the first title slowly. The agent was under explicit deny rules and still got through with three different moves, one of which - stash - is not a flag on git commit at all. Agents reach for these routes in practice. What nobody has is a measured rate, which is why every rate in this piece is stamped as toy.
A presence check is not a behavior check. The script verifies that the string --no-verify appears in a command; it does not verify that no bypass happened. A sibling post makes the same point in a different domain, route-level authorization, where the discipline is knowing what your hook cannot see (the single-authorization gate); the commit boundary deserves the same suspicion.
The anti-hype line this piece owes you. The two-layer local fix is good and you should ship it. It also closes one of the three doors while leaving the other two open, and a gate that reads as solved is worse than a gate that looks weak, because nobody goes looking for a door they believe is closed.
The ecosystem has noticed. block-no-verify-style CLIs (the cline issue above is itself that feature request) and git wrappers shimmed onto PATH that filter the flag before real git sees it both exist now. Both are named here as prior art. Neither has been run for this piece, and a wrapper on PATH carries a trust question of its own.
Three times a hook is the wrong tool at any layer. Judgment calls - is this well-named, does this belong here - do not compile to an exit code; keep them in the rule and in review. Slow checks - a five-minute end-to-end suite - belong on pre-push or CI, once per batch instead of on every commit attempt. A flaky check is worse than no gate: a test that fails at random teaches the agent that red sometimes means nothing, and it will start editing good code to appease a coin flip.
And the terminal layer has its own hole, handed forward below: a required check holds exactly as long as nobody with admin rights turns it off.
The narrow hook’s worst property is that it looks finished.
Back to the table
Section titled “Back to the table”Three curves, one flat. Columns one and two are what every probabilistic layer gives you: better or worse rates, both compounding with N, both ending in the same place if the sprint runs long enough. Column three is the only layer where N is not a variable, and it gets that property from something no local hook can copy: it runs where the commits land, regardless of how they were made.
The principle is not new here. The server is the trust boundary argues it for a different boundary, MCP credentials: the layer you fully control is the one that has to hold, and every layer you do not control is a bonus. This piece applies that argument to a second boundary rather than inventing a new one. The mechanics of the remote layer - branch protection, required status checks, a pre-receive hook - are their own how-to, and this piece does not re-derive them. What it did derive is why that layer is the one where the regress ends.
The solution’s own cost, handed forward: the remote gate is only as trustworthy as the humans who can switch it off. Arithmetic can prove a leak is zero at every N. Arithmetic cannot prove that branch protection still says the same thing on Friday. That question is governance, and it is yours now.
Zero the leak where you own the gate - then go ask who else can turn the gate off.
About the numbers. Every rate in this piece is a toy: the 95% compliance, the 20% block rate, the 5% bypass-attempt rate, and the 50/50 split between the flag and other routes are invented for traceability, sized to be plausible and declared here rather than measured anywhere. No real repo’s bypass frequency is known to the author; the two GitHub issues are cited only as dated proof that the routes exist (verified 2026-08-16), never as rates. Everything else - 0.7737809375, 22.62%, 92.31%, 40.13%, 64.15%, 2.50%, 4.94%, 9.63%, 22.37%, and the flat third column - is exact arithmetic on those inputs, re-checked against a script before publishing. Swap in your own rates and every column keeps its shape: the first two climb with N, the third does not.
For the per-tool mechanics, see Hooks; for the soft layer that explains the gate, see Rules; for checking the hook into the repo so it binds every contributor, see Configuration.

