I maintain claude-forge, a config pack for Claude Code. This post isn't about the pack. It's about one design decision inside it — the adversarial verification loop — and the three defects that decision caught in my own pull requests on a single day in August. I would have merged all three.
The short version of the design: the agent that reviews a change must not share context with the agent that made it. Not because the maker is dishonest, but because review inside a shared context isn't review. It's the same reasoning, replayed.
The failure mode that started this
If you run a coding agent long enough, you notice a pattern. You ask it to implement something, then ask it to review what it implemented, and it approves. Almost always. The approval even comes with a plausible narrative: "I checked the guard against the real files and it passes."
For a while I read this as a discipline problem, something better prompting would fix. It isn't. The maker's context window contains the reasoning that produced the change — the goals it set, the assumptions it made, the story it told itself about why the approach is sound. When you ask that same context to review the change, it doesn't examine the code fresh. It re-reads its own rationale and reaches its own conclusion again. Same context, same blind spot, same verdict.
Human code review solved this problem a long time ago, and not by making authors more careful. It solved it by making the reviewer a different person. The interesting part is which property of "a different person" does the work. It's not different weights — reviewers on a team went to the same schools and read the same style guides. It's a different context: the reviewer wasn't in the author's head while the code was written.
That's a property you can replicate with agents. Cheaply.
The design
In claude-forge v4.0, every behavioral change goes through a loop with two roles that never blur:
- The maker implements, runs targeted evidence (commands, exit codes), and records what it ran.
-
The checker (
adversarial-reviewer) is a separate agent, dispatched with a fresh context. It receives exactly three things: the goal, the spec, and the diff. It does not receive the maker's reasoning, its self-assessment, or its conclusion.
The checker's instruction is the part that matters most: try to break the claim, don't read it. It returns a structured verdict — APPROVE, REQUEST_CHANGES, or UNVERIFIED — and the loop doesn't close until an APPROVE is issued against the revision currently checked out. UNVERIFIED is not a pass. A checker that timed out has told you nothing.
That's the whole design. No new model, no fine-tuning, just a hard wall between two contexts and a gate that only opens from the checker's side. The obvious question is whether that wall actually buys anything. Here is what it bought on 2026-08-18, on PRs #58 and #61 of the repo itself. (The full write-up with the raw review envelopes is in VERIFICATION-LOOP.md; nothing below is hypothetical.)
Defect 1: a guard that passed on its own regression
The repo ships two installers, install.sh and install.ps1, which must install the same set of directories. A directory had once silently disappeared from the Windows installer, so PR #58 added a CI parity step: read the directory names, check each one appears in install.ps1.
I believed the guard worked. My evidence: it passed on the real files.
The checker didn't read the guard. It manufactured the regression the guard exists to catch: copied install.ps1, removed "scripts" from the $directories array, and ran the guard against the sabotaged copy.
Result: missing=0. A false pass.
The guard grepped the whole file for each name, quotes included — and three lines above the array sat a comment warning future editors that omitting "scripts" breaks Windows installs. A comment I had written in the same PR, to make the guard safer. The mention of "scripts" in my own warning comment satisfied the grep, so the guard stayed green while the installer no longer installed the directory.
A guard that passes on its own regression is worse than no guard. It converts an absent check into a green checkmark.
The fix parsed structure instead of text: read install.sh's for dir in loop as the source of truth, extract $directories from install.ps1 through the PowerShell AST.
Defect 2: the fix had the same hole, one level down
Here's the part of the procedure I'd defend hardest: the fix gets attacked as adversarially as the original. I re-dispatched a fresh checker, told it what changed, and asked it to re-break it.
It did. The new AST guard looked up $directories and validated the first assignment it found. PowerShell resolves the last assignment in scope at runtime. So a file with a second $directories assignment that drops "scripts" would pass CI on a value the installer never uses. Same silent-omission shape as defect 1, reached through a different door.
The checker even found it via an asymmetry: the install.sh side of the guard required exactly one loop, while the install.ps1 side accepted "one or more" and took the first. The fix made both sides symmetric — exactly one assignment, or CI fails.
Round 3 was the APPROVE, and it's worth noting what that required: the real files pass, and both sabotaged copies now fail. Reproduced in both directions. "It passes on the real files" — my original evidence — proves nothing on its own, because defect 1's broken guard also passed on the real files.
Defect 3: a dependency ceiling that pinned the past
Same day, PR #61. A dependency bound written from memory:
lxml>=5.2.2,<6
I had researched the floor. The ceiling I just... wrote. It looked like the kind of number that's usually right.
The checker's habit — reproduce, don't trust — applies to claims of fact, not just code. It queried live PyPI instead of trusting the shape of the bound. lxml 6.0.0 had shipped in June 2025; current stable was 6.1.1. My <6 excluded the entire current major and silently resolved installs to 5.4.0, a release from April 2025. The bitter footnote: that PR's own description criticized == pins for causing staleness. My ceiling reproduced the same staleness through a different mechanism.
The fix was <7, and this time every bound was checked against the live index and confirmed with a clean virtualenv resolve.
What the three have in common
None of them was a typo. None would have been caught by a careful re-read — mine or the maker agent's.
- Each was invisible from the inside. Every defect sat exactly where my attention had already been and had already been satisfied. The comment that defeated the grep guard was written by the same hand, in the same commit, as the guard itself.
- Each was caught by reproduction, not reading. The checker built the failure case and ran it, or looked the fact up at its source. "I read the guard and it looks correct" would have approved all three.
- Two of the three were in a fix. Code written under review pressure is where the next defect hides, which is why the loop treats fixes as first-class review targets, not as cleanup.
The honest cost column
This is a design retrospective, so the losses go on the record too.
It roughly doubles token spend on behavioral changes. Every such change runs at least one full fresh-context review, often two or three rounds. The pack routes around this with a task classifier that sizes work S/M/L/XL — a typo fix doesn't pay the tax — but on real changes, you pay.
It's context independence, not model independence. The checker is the same model with a different context. Contextual blind spots — which is what all three defects were — get caught. Weight-level blind spots are shared: if the model can't reason about PowerShell scoping at all, neither agent saves you. Cross-model checking would help there; I haven't measured it rigorously enough to make claims.
The maker still writes the spec. A spec that encodes the maker's misunderstanding gets verified faithfully. The mitigations are partial: the checker reproduces claims at their source (that's what caught lxml), and a finding that comes back twice stops the loop and escalates to a human.
I have no controlled numbers. Three defects in one day of release work is a worked example, not a benchmark. I'm not going to invent a catch-rate percentage, and you should be suspicious of anyone in this space who quotes one without a methodology.
If you want to try the shape
You don't need my repo for the pattern. Any framework that can spawn an isolated second agent can do it: dispatch a fresh checker with the goal, the spec, the diff, and your commands with their exit codes — never your conclusion. Act on REQUEST_CHANGES, re-dispatch fresh, and stop only on an APPROVE against the current revision.
The implementation I run is in claude-forge (MIT), and the worked example with the actual forge.review/v1 envelopes is in docs/VERIFICATION-LOOP.md.
What I'm most interested in hearing: where this breaks for you. Especially if you've tried cross-model checking, or found a class of defect that survives a fresh context.
Top comments (0)