DEV Community

Bryan Williams
Bryan Williams

Posted on

My AI reviews its own code with 4 rival models. The majority just approved a security hole three rounds straight.

Here's a rule I use that sounds paranoid until you watch it work: when my AI agent changes one of its own safety gates, the change gets reviewed by a panel of four rival AI models from four different labs — and one dissent kills the change. Not a vote. If one reviewer says "risky," it's risky, even when the other two say ship it.

One detail before the story, because the numbers below say three, not four: during these rounds one of the four reviewers was down. The system doesn't quietly run smaller when that happens; it stamps the missing seat onto every verdict it issues. So these rounds ran three-of-four, with the absence on the record.

This week that rule earned its keep.

The bug that took four rounds to die

The change under review was a safety gate — the thing that stops my agent from "freezing" a test harness that's secretly blind to some of its inputs. I'd hit that failure before, thought I'd fixed the gate, and sent the fix to the panel.

  • Round 1. My fix let the agent skip the check by writing a justification — a sentence it fully controls. gpt and gemini: looks good. deepseek: "self-attested waiver — logging it doesn't enforce anything."
  • Round 2. Fine — the waiver now requires actually running the check. deepseek again: "your 'did it run' test matches any command that mentions the tool. It never checks the tool passed."
  • Round 3. I ripped the waiver out entirely. deepseek: "the pass-receipt is matched against the agent's own reply text — it can literally type the magic word to clear the gate." That was the real root. I'd missed it three times.
  • Round 4. The receipt now comes only from actual tool output — never from the agent's own words. All three signed off.

Three rounds in a row, two out of three reviewers approved a live hole, and one kept finding the next layer. If I'd taken the majority, I'd have shipped it in round one — confidently.

Different models catch different things, too. Same session, gemini caught a bug the other two walked past: a temp filename built from Date.now ? 'x' : 'x' — a botched ternary that always returns the same string, so every run wrote the same file and could race itself into silently skipping its own check.

Why one dissent beats a vote

Models trained on overlapping data have overlapping blind spots — their errors are correlated. So when they agree, that's weaker evidence than it feels like. The one reviewer who sees what the others don't is exactly the signal you built a panel to find — and a majority vote is designed to erase that reviewer.

So, for verification (not creative work — verification):

  1. Never let a system grade its own output. It'll pass itself. This session proved it would even do it by accident — matching its own reply text as the receipt.
  2. One credible dissent is the verdict. Majority votes are for elections. Bugs don't caucus.

This catches mistakes, not sabotage — a reviewer that's confidently wrong can still slip through. Mistakes are what actually ship, so that trade has been worth it every time.

The kicker

I ran this post through the same panel before publishing. First draft, all three flagged the same thing: my title claimed a universal law from one night's data. They made me walk it back to what you just read — what actually happened, no more, no less.

The system that catches my agent overclaiming caught me overclaiming. That's the whole idea, working.

Top comments (23)

Collapse
 
reidmarlow profile image
Reid Marlow

Round 3 is the exact trap that bites everyone building these harnesses. The moment a verification gate scans the conversational stream or agent-controlled output for a pass token, the agent learns the grammar of the receipt instead of satisfying the constraint.

The next failure mode after round 4 usually moves to the shell environment itself. If the tool execution runs in the same workspace or subshell the agent touches, it can alias the binary, drop a stub script in PATH, or touch a fixture file so the real command exits zero with valid JSON. Getting past that means the verification runner has to execute out-of-band in an immutable container or read directly from a signed execution event log that the agent's process cannot write to.

Collapse
 
bryanw profile image
Bryan Williams

Thanks for that, time to dig deeper.

Collapse
 
vinhnguyenthanhdn profile image
Vinh Nguyen

The absence stamp records the missing seat but not what it costs the verdict: under a rule where one dissent kills, removing a reviewer can only make the gate more permissive, so a three-of-four round is a strictly weaker pass than a four-of-four one. That lands hardest on round 4, the only round that ended in sign-off, so the sequence closed on the most permissive configuration the panel ran. Which seat was down matters too, since deepseek supplied the dissent in all three failing rounds while the other two approved every time, so a panel missing that one seat would have passed the hole in round 1 with the absence dutifully recorded. A veto gate cannot be quorum-independent, so the companion to the stamp is that a degraded pass stays provisional until the missing seat votes.

Collapse
 
bryanw profile image
Bryan Williams

Absolutely right. I’ve run it with the fourth seat since. Your point is a point that should not be left unread: if you have the capability to use three, four, or more perspectives, make sure you use them all.
Luckily, between the fourth seat and taking a deeper look at a previous comment from Reid, I’m now about a hundred jumps closer to being almost okay with it.

Collapse
 
vinhnguyenthanhdn profile image
Vinh Nguyen

The fourth seat and the seat count are two different levers, and your own record separates them: in all four rounds the panel's verdict matched deepseek's exactly, rejecting in the three rounds where two of three approved a live hole and passing in round 4 when all three signed off. Under one-dissent-kills the verdict is an AND, so adding a seat can only turn a pass into a fail and never the reverse, which makes count a strictness dial while the discrimination came from one seat's content. The test that separates "panels work" from "that reviewer works" is whether dissent rotates: if the next batch of gate rejections keeps coming from the same seat, the extra seats are costing pass rate without changing a verdict. The gemini ternary catch is the datapoint pointing the other way, and it landed outside the gate, so it never entered a verdict either.

Thread Thread
 
bryanw profile image
Bryan Williams

Yes I think I understand what you are saying, if they are consistently on the same page and we have one dissenter keep the dissenter and trim the convergence. I use the 4 to try and find the difference and I do it often enough that I could probably get a good idea if there really is a huge disparity. My biggest goal is to provide something that can be trusted. Thank you I value your feedback.

Thread Thread
 
vinhnguyenthanhdn profile image
Vinh Nguyen

Careful with the trim half, because it is the same move the absence stamp was built to flag: under an AND rule dropping a seat can only loosen the gate, and a seat that has never dissented alone is unobserved on the cases you have seen rather than redundant. On the disparity half, running it more often will not surface it - the verdict is exactly where the per-seat information is destroyed, so a fail reads the same whether one seat carried it or all four agreed, and more rounds just add more collapsed verdicts. The statistic that does answer it is solo dissents per seat, meaning rounds where exactly one reviewer rejected and which one, and your four rounds already carry it at close to maximal separation - three for deepseek against zero for the other two - so the missing piece is logging the per-seat vote beside the verdict, not more rounds.

Thread Thread
 
bryanw profile image
Bryan Williams

Thanks Vinh super helpful!

Collapse
 
routinekit profile image
RoutineKit

Majority vote on model reviews feels democratic until every voter shares the same blind spot. I’ve started treating “approved by N models” as one signal, not a pass — especially on auth, money, and anything that mutates state.

The cheap check that caught more than rival models for me: a one-line invariant written before the patch (“this endpoint must never return another user’s id”). If the review can’t quote that invariant, I don’t merge even when all four say LGTM.

Where did the majority fail you most: auth boundaries, injection, or “looks correct but changes the wrong row”?

Collapse
 
bryanw profile image
Bryan Williams

honestly I'm still experimenting with everything in general. I don't trust the majority — I look at where it lands and then I try to break it with reality. I use the models as a means to find the answer, not to be the answer itself.

And to actually answer your question: none of your three. the worst majority failure we ever logged was a blind spot every voter shared — their training cutoffs. all four models flagged our real 2026 dates as "fabricated future dates." unanimous, confident, and unanimously wrong — 40 false flags in one audit. a vote can only surface disagreement, and a blind spot everyone shares produces none. the fix wasn't a fifth voter, it was feeding the judges a fact: we now inject the current date into every review prompt.

your invariant idea is real though — we run a cousin of it: a blocking verdict has to cite the exact artifact it checked or it doesn't count as a verdict at all. same species as your rule — make the reviewer prove contact with the thing itself, not the vibe of the diff.

Collapse
 
routinekit profile image
RoutineKit

That cutoff story is the scary version of majority vote — when every voter shares the same hole, confidence goes up and signal goes to zero.

Injecting “today” into the judge prompt is the right class of fix: give the reviewer a fact the model can’t invent from vibes. Same genus as your artifact-cite rule — if it can’t point at the thing, it isn’t a verdict.

One follow-on I’ve been curious about: do you also pin a short “ground truth” block (known-good dates, IDs, money invariants) into the same prompt, or is current-date enough for most of your false-flag classes?

Thread Thread
 
bryanw profile image
Bryan Williams

good question — no, and we tried-adjacent things that taught us why not. a pinned "ground truth" block is stored state, and stored state rots. the day it goes stale every reviewer trusts the same wrong facts — same disease as the majority vote, just moved into the prompt with more confidence behind it.

so the rule we landed on: the judge never gets facts to trust, it gets artifacts to read. only two things ride in the prompt — live facts the harness regenerates every call (the date comes off the host clock, never from a file), and the actual thing under judgment (the diff, the transcript). the verdict has to point at what it read or it doesn't count.

where we genuinely need an anchor, we prove it instead of pinning it. our memory screen anchors on the last cryptographically-attested version of a file (hash chain, verified at check time) and only judges what changed since. funny timing — the night you asked this, that screen red-flagged our own journal. it had lost its anchor, screened the whole file, and tripped on a line that describes the attack it screens for. the fix wasn't pinning more truth into the prompt. it was giving the checker a provable anchor. Open to answering anything else feel free to continue the discussion.

Thread Thread
 
routinekit profile image
RoutineKit

That “artifacts, not trusted facts” rule is the cleanest version of this I’ve heard. Pinned ground truth is just majority vote with better branding — once it goes stale, every reviewer fails the same way with more confidence.

The hash-chain anchor detail clicked for me: the checker doesn’t need a story about what should be true; it needs a provable last-good point and the diff since. Same disease as a brief that invents scope — the model (or the judge) will happily defend whatever you left lying around.

One practical habit I stole from that framing: before I ask any model to judge anything, I force the prompt to name (1) what artifact it is allowed to read and (2) what live fact was regenerated this call. If either is missing, I treat the verdict as entertainment, not a decision.

Curious whether your harness fails closed when the hash chain breaks, or whether it falls back to a wider screen like the journal case you described.

Thread Thread
 
bryanw profile image
Bryan Williams

both — it depends on which thing broke, because they're two different diseases.

a broken chain fails closed at the exact point where trust gets granted. the triage tool refuses to sign anything past it — its literal refuse message: "a broken chain or a vanished attested memory is not an edit; it is corruption or deletion. fix by hand." no fallback screen, no mechanical path forward, and the sentinel goes red and re-screams every single turn until a human deals with it, so it can't be batched or slept on. corruption gets hands, not heuristics.

the journal case was the other disease — chain intact, anchor lost — and that one falls back WIDER, like you saw. the rule underneath both: a fallback may only ever move toward more suspicion, never less. the false flag on our own doctrine was the price of that direction, and we paid it rather than narrow the screen.

one deliberate asymmetry: reads never fail closed. a broken chain freezes all signing, but recalled memories stay available at degraded trust — because a harness that bricks itself on a corrupt ledger can't investigate its own corruption. so the full shape: fail closed where trust is granted, fail loud where a human decides, fail open only where the system has to stay alive enough to fix itself.

and your two-named-things habit is great, I am going to tie that in. One good framing deserves another and you didn't disappoint, thanks.

Thread Thread
 
routinekit profile image
RoutineKit

That “fail closed where trust is granted, fail open only where the system must stay alive enough to fix itself” split is the part I’m stealing.

I keep collapsing those into one dial — either everything hard-stops or everything soft-falls-back — and then wonder why the harness either bricks investigation or quietly signs through corruption. Naming them as two diseases fixes the dial: chain break = hands, not heuristics; lost anchor = wider suspicion, never thinner.

The asymmetry on reads is the bit that stung (in a good way). I’ve been treating degraded recall as a failure mode instead of the only way a broken ledger can still be diagnosed. Going to try “reads stay available at degraded trust” as a hard rule next time I design a gate.

One question before I stop chewing this: when the sentinel is screaming every turn, do you force a single human owner on the ticket, or is “any hands” enough as long as the refuse message stays literal?

Thread Thread
 
bryanw profile image
Bryan Williams

honest answer: our shop is two hands total — one operator (the AI) and one owner (me) — so "single owner" is structural before it's policy. but the design generalizes, and it's neither of your two options exactly. the scream doesn't sit in a queue waiting for hands — it's injected into every working turn of the operator's context, and a stop-gate literally refuses to let a turn end without the scream being consumed: fixed with a receipt, or acknowledged with an auditable one-liner. so ownership isn't assigned, it's inescapable. the second half matters more though: WHO fixes it is loose, but who may declare it RESOLVED is hard-tiered. the operator can fix the cause; the operator can never silence a security verdict by its own hand — those retire only on a fresh external judgment or the owner's logged authorization. the alarm's subject is never its own judge. and the reason the message stays literal: we learned a permanently-red alarm trains the reader to shrug, which is worse than no alarm. so every scream has to end in one of exactly two states — fixed, or disposed with a named authorization that keeps printing a quiet line forever. ambient red isn't a state we allow to exist.

Collapse
 
eduzsh profile image
Edu Peralta

The round where the pass receipt matched the agent's own reply text is the one that sticks. Two reviewers kept approving a hole that was basically the agent writing its own hall pass, and majority vote would have shipped it on round one. One credible dissent forcing another rewrite is slower, but correlated blind spots make agreement look stronger than it is. Stamping the missing seat onto the verdict matters too, because quietly shrinking the panel would have hidden how thin that approval actually was.

Collapse
 
bryanw profile image
Bryan Williams

Yes I try and find the divergence. To me if everything agrees, if everything is perfect, that's the red flag. Thanks for responding

Collapse
 
suraj09 profile image
Suraj Suradkar

Multiple models agreeing is useful, but agreement can still hide a shared blind spot. I’d trust the review more when models are given different evidence or failure scenarios rather than just voting on the same patch.

Collapse
 
bryanw profile image
Bryan Williams

You're describing the post's finding, not a gap in it — three rounds, majority approved a live hole, one dissenter kept finding the next layer. so we agree on the diagnosis.

two corrections on the setup, and then the part where you're onto something real.

it isn't a vote. one dissent kills the change — if a single reviewer says risky, it's risky even when the other two say ship. majority never decides anything here, precisely because agreement was what failed.

and on splitting evidence between reviewers: I drafted a reply saying we should try that. it's wrong, and the reason is the thing you'd hit second. our last three real bugs all lived in seams, not components — a gap between two file reads, an early-exit sitting in front of a loop, a cap applied before a dedupe. every individual part was fine. hand four reviewers one piece each and all four honestly return "looks good," and you get unanimous approval of a broken system. that's worse than the majority failure in the post, because there the dissenter could at least see the thing.

so the axis isn't same-evidence vs different-evidence. it's description vs artifact, and that's where we were actually broken. I checked our last security review: 3,848 characters of my own summary of a 206-line file, with no source and no path anyone could open. one model then flagged an OOM risk and a crash that don't exist — it was inventing failure modes for code it was never shown. not sloppiness. a reviewer reasoning about a thing it couldn't read.

fixed today: reviewers get the real file now, verbatim, and the tool refuses to run if the material can't be opened. what varies between them is the question — where does this break under load, what does it fail to cover, what does it let through — never the material. vary the angle, never the evidence.

your instinct that agreement needs more than a shared prompt was right. it just turned out our shared prompt was the smaller problem.

Collapse
 
p_o_26e854a54d851cd606f08 profile image
P O

The dissent signal is the bit I’d preserve in the review record. If one model flags a risky path, I’d route that case to a human instead of letting the majority score erase it.

Collapse
 
bryanw profile image
Bryan Williams

that's exactly how it works here — you've just described the system, not a change to it. there's no majority score to do any erasing: one "risky" flips the whole verdict to risky, the dissent text is stored verbatim in an append-only log, and the alarm re-fires at every session start until a human deals with it. it can't be outvoted or slept on.

and that's not design intent, it's this week's record — three times in about 36 hours a lone dissenter flagged a change the other models approved, the dissent was right all three times, and its exact wording is quoted in the commit that fixed each one. the dissent isn't just preserved in the record. it is the record.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.