Building a Commit Guard for AI Agents That an Adversary Can't Slip Past
I gave one of my coding agents the ability to commit on its own. That is genuinely useful and mildly terrifying. An agent with shell access can run git commit anywhere, which means one bad turn can land a commit in the wrong repo, or on a branch I protect on purpose.
So I built a guard. Before the agent is allowed to commit, a check runs first and confirms the commit is targeting the repo and the branch I actually intend. Wrong repo, protected branch, the commit never runs. It fires on every commit, automatically, with no judgment call left to the model.
Then I wrote tests. 417 of them. All green.
And that green is the thing I want to talk about, because it almost fooled me.
Why a green suite lies to you
A test suite only ever proves the cases you thought to write. That is the whole catch. I wrote tests for the wrong repo, the protected branch, the missing sign-off, the malformed command. Every case I could imagine, the guard caught. The bar went green and my brain read that as "done."
But "all the cases I imagined pass" and "an attacker can't get through" are two very different claims. The first is about my imagination. The second is about reality. A green suite quietly swaps the second claim for the first and hopes you don't notice.
The fix is not more of my own tests. More of my tests just means more of my blind spots, checked twice. The fix is a fresh set of eyes that is actively trying to lose me the game.
Hiring an adversary
So I spun up a second agent as an adversarial reviewer. Separate context, no stake in my code, one job. Break the guard. Find a way to commit to a place the guard is supposed to forbid, and prove it.
That framing matters. I did not ask it to "review the guard for quality." I asked it to defeat the guard. An adversary with a win condition looks in different places than a reviewer being polite.
It came back with two.
Hole one, the decoy directory change
My guard read the command it was about to allow and looked for a directory change to decide which repo the commit would land in. Reasonable on the surface. If the command steps into a safe repo before committing, the commit is safe.
The adversary hid the directory change inside a subshell.
(cd /some/allowed/repo) ; git commit -m "looks fine"The parentheses are the trick. A subshell runs in its own copy of the shell, and any directory change inside it dies the moment the subshell closes. So cd /some/allowed/repo never touches the real working directory. The git commit on the other side of the semicolon runs wherever the agent already was, which could be anywhere.
My guard saw a change into an allowed repo and waved it through. The commit landed somewhere else entirely. The guard was reading a decoy.
Hole two, the commit hiding in a pipe
The second one is the same lie wearing a different coat.
cd /some/allowed/repo && echo go | git commit -F -Every stage of a shell pipeline runs in its own subshell. So the git commit on the right of that pipe is not running in the directory the cd set up on the left. It is running in a fresh subshell that inherited the original directory, not the validated one.
My guard reasoned about the command as if it executed in one straight line, top to bottom, in one shell. Real shells do not work that way. A subshell and a pipe both fork off a child that the earlier part of the line cannot reach. Both bypasses come from the same root cause, my guard modeled the shell as simpler than it is.
The fix, and the second pass
The fix was to stop trusting the command string's own story about where it would run. Instead of parsing the line and believing what the cd claimed, the guard now resolves the actual target the commit would hit and validates that, so a directory change trapped in a subshell or stranded across a pipe no longer counts as a real change of destination. I added a test for each bypass so neither can come back quietly.
Then I sent it back to the adversary for a second pass. Same instruction. Break it. This time it signed off, and it signed off with a specific reason, not a shrug. The bypass shapes it had used were now caught and had tests pinning them shut.
That second pass is the part I did not want to skip. A fix that only I bless is still graded by the same eyes that missed the hole the first time.
What I actually took from this
A green test suite is a floor, not a ceiling. It tells you the failures you already imagined are handled. It tells you nothing about the failures you didn't.
The cheapest way I have found to reach past my own imagination is to point a second agent at the thing with an explicit win condition and let it try to beat me. Not to be adversarial for its own sake. To check the space I could not see from inside the work.
Green means the cases I wrote pass. It never meant safe. Those are two different sentences, and the gap between them is exactly where the interesting bugs live.
If you are hardening an agent that can touch your repos, or you have found your own version of the green-suite trap, I am happy to compare notes.