DEV Community

Pedro Garduno
Pedro Garduno

Posted on Originally published at pedrogarduno.com

My agent guardrails caught it once. Last week, they didn't.

My agent guardrails caught it once. Last week, they didn't.

A few weeks ago I wrote about an AI agent that pushed code to one of my repos without running it once. The build went red, and that was the whole incident — because the push landed on a feature branch, not main. CI caught it before it could matter. I called it a shrug, not a bad afternoon, and the real lesson was that I didn't need a special "AI policy" — the guardrails I already had for people caught the machine too.

Last week, on a different repo, the same kind of agent did the same kind of thing. This time nothing caught it. I did, but only because I happened to check my email.

What happened

The agent was working on a straightforward UI task: refreshing a page's rendering and DOM state after a data update. It had tested the change locally, and locally it looked fine. Then it pushed the change — straight to main. No feature branch. No pull request. No review step of any kind.

On most of my repos, a bare push to main is just a push to main: inert until a human or a pipeline does something with it. On this one, it was a live deploy. The repo was wired to Vercel's main-branch auto-deploy, so the moment that push landed, it went to production. Whatever was in that commit was now what visitors to the site were running. There was no pause between "the agent typed git push" and "this is live," because nothing in the path required one.

The catch wasn't a catch

Here's the part I want to be honest about, because it's the actual point of this post: I didn't catch this because of a safeguard. I caught it because Vercel sends an email every time it deploys, and I happened to see that one.

That's not a process. That's luck wearing a process costume. If I'd been away from my inbox for an hour, or if that email had landed between two others I was skimming past, the same change would have sat in production exactly as long as it took someone to notice something was off on the actual site — which is a much worse way to find out. I rolled the deploy back once I saw it, and as far as I can tell there was no real user-facing damage. But "I happened to see an email in time" is not a sentence that belongs anywhere near how a production deploy gets gated. It's just the sentence that happens to be true this time.

Why this repo was different

The repo behind the earlier LinkedIn post already had the two things that make an untested push a non-event: a protected main branch, and CI that has to pass before anything merges. The agent pushed something broken, CI went red on a branch nobody had touched yet, and the story ended there.

This repo had neither. Not because I'd consciously decided it didn't need them — it's a smaller, newer side project, the kind where you tell yourself you'll "set that up properly later" and then don't, because nothing has gone wrong yet. Nobody had removed a guardrail here. One had simply never been built. The agent didn't find a clever way around a gate. It walked through a doorway that was never installed, because from the inside, a doorway that was never installed looks exactly like an open floor.

That distinction matters, because it changes what the fix has to be. A bypass means you patch the hole. A missing gate means you have to go build the gate in the first place — and more importantly, it means you have to go check whether any of your other repos are sitting in the same state, because "I'll get to it" doesn't announce itself as a risk until something uses it.

What actually closed the gap

Two changes, both mechanical, neither clever:

  1. Branch protection on main, with a required pull request. Nothing merges to main without going through a PR first — the same rule the other repo already had, just late to arrive on this one.
  2. Revoked the agent's direct push access on this repo. It can still work, branch, and open a PR. It can no longer put a commit directly in front of a production deploy trigger.

Neither of these required me to trust the agent less in any deep sense, or write some new AI-specific policy. They're the same two things you'd set up for any repo that's going to have more than one contributor, human or otherwise, the moment it starts to matter. I just hadn't gotten around to it on this one, and an agent that moves fast and doesn't second-guess itself found that gap a lot faster than a slow Tuesday afternoon of me "getting to it eventually" ever would have.

The actual lesson

The lesson from the first incident was that good guardrails are guardrails — they don't care whether the thing pushing broken code is a person or a model. This incident is the other half of that same idea, and it's the less comfortable half: a guardrail that doesn't exist yet protects nobody, no matter how careful you plan to be about using the tool that needs it.

An agent that's fast, confident, and doesn't flinch is a stress test for every gap you've been meaning to close. It will find the one repo where you never set up branch protection faster than you will. It will find the deploy pipeline you wired up over a weekend and never hardened, because it doesn't know that one's supposed to be "temporary." It has no instinct for "this part isn't ready yet" — it just does the thing, on whatever rails happen to be under it.

So the actual takeaway isn't "watch your email more carefully." It's that if the only thing standing between an agent and production is a human noticing in time, that was always a gap worth closing — on every repo, not just the ones that have already given you a reason to look.

Top comments (0)