Gating Your Agent's Commits With pre-commit — Keeping Broken Changes Out of the Main Repo
How to wire up a pre-commit gate that lints, type-checks, runs fast tests, and scans for secrets the moment Antigravity's agent commits — with measured timings and the ordering that keeps it fast.
I once asked an agent to fix a bug, and the next morning the green tests in my commit history had turned red. It was a repository for an app I run as an indie developer.
The bug it was asked to fix was genuinely fixed. But somewhere along the way it had deleted a single import in an unrelated file, and an unrelated module now crashed on startup.
The agent was not at fault. The real cause was that I had left myself in a state where I could commit generated changes without reading them.
A human review on every change would be ideal, but when you hand work to an agent many times a day, reading every full diff each time does not last. So I lean on pre-commit. If you concentrate inspection at the single point of the commit, both agent output and your own manual edits pass through the same net, every time.
Why the commit moment is the right checkpoint
An agent's work rewrites files one after another. Intermediate states are broken by definition, and inspecting them there means nothing.
What you want to inspect is the moment the agent decides "this is a stopping point" and tries to finalize the change. That moment is exactly git commit.
You could inspect in CI instead, but CI runs after the push. By then the broken commit already sits in local history and the agent has moved on to its next task. With pre-commit, a broken change never even becomes a commit. The win is that it closes inside the agent's own working loop.
I use the same idea running my blogs. In the pipeline that auto-updates the Dolice Labs sites, every article passes through a Python quality gate right before push, and any violation means the article is thrown away and rewritten. Placing inspection just short of the final destination is the same instinct whether the artifact is code or prose.
Start from the smallest config
If you stack heavy hooks from day one, the agent's wait time stretches and you stop wanting to use it. Begin with only what is fast and high-impact.
Create .pre-commit-config.yaml at the repository root.
Running pre-commit install once replaces .git/hooks/pre-commit, and from then on every git commit carries the inspection. When the agent calls git commit internally, the same net fires — no special setup is needed on the agent side.
At this point, commits containing syntax errors, leftover merge markers, or an accidental huge file all stop at the commit stage. In my measurements, this minimal hook set finishes in under a second when only a handful of files are staged. It does not register as wait time.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A working .pre-commit-config.yaml that rejects a broken change in 1 to 3 seconds, before it ever becomes a commit
✦How to feed the failing hook output back to the agent so it self-corrects without a human in the loop
✦The order to stack secret scanning, type checks, and fast tests so coverage grows without adding wait time
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Passing syntax alone does not prevent the "deleted an import and broke another module" class of accident from the opening of this article. Add a type check and a very short test run.
For a TypeScript project, call tsc as a local hook.
- repo: local hooks: - id: typecheck name: tsc --noEmit entry: npx tsc --noEmit language: system pass_filenames: false files: \.(ts|tsx)$ - id: fast-tests name: vitest (changed only) entry: npx vitest related --run language: system pass_filenames: true files: \.(ts|tsx)$
The key here is vitest related --run. It selects only the tests connected to the staged files, so it is dramatically faster than running the whole suite each time. On a mid-size project of mine (about 320 tests), a full run takes around 18 seconds, while the related-only run settled at an average of 2 to 4 seconds.
I attach pass_filenames: false to the type check because tsc needs to see the whole project. The tests, conversely, receive file names to narrow down to the related set. This split is what widens coverage without lengthening the wait.
Stop secret leaks at the last wall
Agents sometimes expand the contents of .env into sample code while debugging. I have broken into a cold sweat more than once when something that looked like an API key got caught just before a commit.
This is the exception to the "fastest first, heaviest last" rule. Secret scanning is the one check I always want to pass, even if it is a little slow. If an earlier hook rejects the commit, gitleaks is never reached, so nothing is wasted either.
The recommended hook order looks like this.
Order
Hook
Role
Rough time
1
format / basic checks
whitespace, newline, conflict markers, large files
< 1 s
2
ruff / eslint
unused imports, syntax, style
1 to 2 s
3
tsc / type check
type mismatches, broken references
3 to 8 s
4
vitest related
only tests touching the change
2 to 4 s
5
gitleaks
secret leak prevention
1 to 3 s
Even summed, it stays in single-digit seconds. Inserting this much inspection into one agent cycle barely interrupts the felt flow.
Feed the failure back to the agent so it self-corrects
This is where human-centric and agent-centric operation diverge the most.
When pre-commit fails, git commit returns a non-zero exit code along with the failure log on standard output. If you let Antigravity's agent handle the commit, feeding that failure output straight back to the agent usually sends it back to read the cause and fix it itself.
What I use is a sentence placed in the agent's instructions ahead of time.
If a commit fails on a pre-commit hook, read the emitted error,fix the relevant file, then commit again.Bypassing hooks with --no-verify is forbidden.
Spelling out the --no-verify ban is the crux. Without it, the agent can make "getting the commit through" its goal and take the shortest path that skips inspection. A gate only works as a gate once the bypass is sealed.
The real loop runs like this: the agent deletes an import, tries to commit, ruff reports an unresolved import or tsc raises a reference error, the commit fails, the agent restores the line, and the recommit passes. With no human stepping in once, I stopped finding red tests in the morning.
Push heavy checks down to pre-push
Cramming everything into commit time eventually makes it slow and the whole thing collapses. Move any check that does not finish in seconds to the pre-push stage.
- repo: local hooks: - id: full-test-suite name: full vitest entry: npx vitest --run language: system pass_filenames: false stages: [pre-push]
A hook tagged stages: [pre-push] does not run on commit, only on push. Enable pre-push once with the following.
pre-commit install --hook-type pre-push
Now commits stay light and fast, while the full suite runs at the final gate before anything goes to the remote. It pairs well with an agent that carves many small commits, holding down total wait time while letting nothing broken reach the production branch.
The monorepo pitfall where hooks fire blanks
In a monorepo holding several packages, a local hook in the root .pre-commit-config.yaml can fire blanks because it cannot find the command for the package you intended. npx tsc reads the root config and never looks at the changed sub-package's tsconfig.json.
In that case, I recommend slipping in a small wrapper that narrows inspection to the changed packages.
#!/usr/bin/env bash# scripts/typecheck-staged.shset -euo pipefail# Find the affected packages from staged filespkgs=$(git diff --cached --name-only | grep -oE '^packages/[^/]+' | sort -u)[ -z "$pkgs" ] && exit 0for p in $pkgs; do echo "typecheck: $p" (cd "$p" && npx tsc --noEmit)done
Point the hook's entry at this script. Because untouched packages are not inspected every time, the wait does not balloon even in a monorepo. After I switched to this shape on a layout with eight sub-packages, the type-check wait shrank to about a third on average.
What to leave to the gate, and where the human still looks
pre-commit fully seals off the "mechanically detectable" ways of breaking. Syntax, types, tests, and secrets can all be stopped here.
What it does not guarantee is whether a change that passed is actually what you intended. Misread specs, and behavior in areas with no tests, still need human eyes. Once I drew this line clearly, my review burden dropped sharply, because I only had to look at green diffs from a design standpoint.
The faster you spin the agent, the more inspection automation pays off. Precisely because the volume of hands-on change grows, keep a single wall at the commit moment that refuses to finalize anything broken. It is unglamorous, but for me it is the mechanism behind a calm start to each day.
I hope it helps with your own setup. Thank you for reading.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.