We were closing a security hole. It let you run a root level shell through Discord, which is too dangerous to leave open. The fix looked straightforward: restrict that worker's permissions down to read only, no running commands, no editing files. We wrote it, read it back once, and it looked fine.
But when we handed the same code to a second model to review, it caught in seconds that in the permission list we'd just written, the shell command showed up on both the "allow" side and the "deny" side at once. The hole we thought was sealed was actually still half open, because two config lines contradicted each other. Deleting the conflicting line was the fix, and only then was the hole really closed.
The scary part is we wrote that fix ourselves, read it back, and still didn't see it. Our head had already decided "this is the code that closes the hole," so our eyes slid right past the line that contradicted it. What caught it wasn't us getting more careful. It was a second model that didn't carry that assumption.
Why authors can't review their own work
When AI writes code for you, it doesn't just type it out. It holds a reason for every line, why it wrote it that way. Ask it to go back and read its own work, and that same set of reasons is still there, so it reads right past the wrong parts. From its point of view, every line already has an explanation.
The model isn't trying to lie. It's just good at finding reasons to back up what it already did. Point that skill at reviewing its own work and it becomes the lawyer for its own code, not the reviewer.
The fix isn't telling the same model to "review more carefully." The problem isn't carefulness, it's the assumption it's carrying. What works better is handing the code to another model, one that didn't write it and isn't carrying the original reasons, so it sees what the author missed.
This is the same principle human teams have used for ages: the person who writes and the person who reviews shouldn't be the same person. Move that into work where AI writes most of the code and it still holds, maybe more than before, because AI writes faster and more confidently than people do.
The second catch: a deploy that would ship the future early
The other time the reviewer saved us was on the script that controls deploys for this blog. The system is built to publish one post a day, off a queue. We wrote the part that copies the whole folder up to the server before deploying, and figured that was that.
When we ran the diff past Codex, it pointed at something we hadn't seen at all. If the queue holds several posts that are approved but not yet due, copying the whole folder and deploying once would push every future post live at the same time. The one a day cadence we'd designed would break instantly.
In hindsight it's obvious. But while writing it, we were focused on "make today's post deploy," and never thought about the other files riding along in the same folder. The reviewer marked it P2. We fixed it by holding back the not yet due posts before the deploy, then putting them back once it finished.
The thing to notice: these two bugs are completely different. One is security, the other is deploy logic. But they share one trait. The code ran clean, no errors. And without a second pair of eyes, both would have shipped to production.
The second model isn't always right
By now this sounds like a second model is the answer to everything. It isn't. There was another round where the reviewer raised something as a P2 in our message routing. We read it, checked it against the existing tests, and the real path wasn't broken the way it warned. What it caught was a very narrow case, and arguably correct as intended anyway. In the end we didn't change anything.
This is the part that matters. The second reviewer isn't the boss, it's a second opinion. Its job is to flag where you should stop and look, not to decide for you. Everything it raises, you still weigh against the real code and the real tests yourself. Taking it all on faith is about as dangerous as not reviewing at all.
So we treat what the reviewer flags as a list to go verify, not a list to go fix. Only the ones that survive verification get changed. Two of the three times here, it found something real; the third, it over warned. That's a usable rate, as long as a human is still the one confirming.
Put a second model on every diff before merge
The principle from all three is one sentence: before every merge, hand the diff to a model that didn't write it. The rest is how to make that happen every time, not just when you remember.
What we set up is only a few things. First, it runs headless: one command, done. The reviewer reads the repo, reads the diff, runs the tests itself, and hands back findings point by point with a severity. Because if you have to open a chat and talk to it every time, eventually you skip it.
Second, the review never downgrades the model to save money. For other tasks we pick the model by difficulty. But for review we force the strongest model with the highest reasoning, always. The whole point of this step is to catch what the author missed; letting the reviewer miss it too is pointless.
The second model we use is OpenAI's Codex, because it ships a review the diff command already, and more importantly because it comes from a different vendor than the one that wrote the code. A different vendor means a different set of blind spots, which is the entire reason we put the two against each other.
If you want to try it, start with one spot. Next time, before you merge work the AI wrote for you, don't take the author's word for it. Open a second model, hand it the diff, and ask one question: what would the author have missed? That alone is enough to see why the writer and the reviewer shouldn't be the same one.
Written from real work. The full version, with a Thai edition, lives at productize.life.
Top comments (5)
The mechanism is sharper than fresh eyes: the reviewer helps because it lacks the generator's plan in context, so it never inherited the assumption that the hole was sealed. That suggests an operational rule most second-model setups miss: don't hand the reviewer the author's rationale or chain of thought, give it only the diff and the spec. The moment you paste in the why, you re-infect it with the same commitment and it starts defending the code too. One caveat on your specific bug though: an allow and a deny on the same command is a logical contradiction a deterministic linter catches for cents, no model needed. I'd layer it: static checks for contradictions the reviewer shouldn't have to notice, second-model review for the semantic bugs a linter can't reach. Where do you draw that line in your gate?
You're right about the linter — an allow and a deny on the same command is a contradiction I shouldn't have needed a model to catch, and I've since pushed that class of check down to the cheap deterministic layer. The line I draw now: anything with a definite right answer (contradictions, schema, types) goes to static checks; the second model only gets what a linter can't reach — did this actually do what the spec asked, does it handle the case nobody wrote down. And yes, exactly your rule: the reviewer gets the diff and the spec, never the author's reasoning. The one time I pasted in the "why," it started defending the code instead of testing it. Cheap checks catch contradictions, the second model catches intent, the human catches the business rule neither knew about.
This matches what I've seen running multiple agents side by side. The generating model and the reviewing model share the same blind spots way less often than two runs of the same model, so a second pass from a different provider genuinely catches things a self review misses. The part I'd push back on a little is treating the second model's approval as the finish line. I still open the diff myself before it merges, because both models can agree on something that's wrong for a reason neither has context for, like a business rule that only lives in someone's head. Curious whether your two bugs would have surfaced from a human skim of the diff too, or if they were the kind only a second model would catch.
Agreed — approval isn't the finish line, it's the last cheap filter before I open the diff myself. Two models agreeing on something that's wrong for a reason neither has context for is exactly what the human skim is still there for. On your question: of the two, one you'd catch on a careful skim (the allow/deny contradiction — honestly a linter should've caught it before either of us). The other read as correct — it looked right, did the plausible thing, and only surfaced when a model with no stake in my plan asked "but does this match the spec?" That's the one I couldn't have skimmed my way to, because I'd already decided it was fine when I wrote it.
The layered approach makes sense. Let linters catch obvious config mistakes, then use AI for the messy logic paths.
The value is not replacing review, it’s getting another angle before merge.