An agent reviewing its own diff agrees with itself. A different model reading the same diff cold does not, and it is the cheapest quality step available: review is mostly input tokens, and input is the cheap end of every rate card. How to wire a reviewer, what to put in its prompt, and what it reliably misses.
why an agent reviewing its own diff agrees with itself
Ask an agent to review the code it just wrote and it will mostly tell you the code is fine. Not through vanity. Through context. Everything that led to the current diff is still in its window: the reasoning that chose this approach, the constraint it decided was acceptable, the shortcut it decided was deliberate. Asked to review, it re-reads its own justification and finds it persuasive, which is unsurprising, because it wrote it.
A second session reading the same diff gets none of that. It sees the code and no story. So the questions it asks are the questions a reader asks rather than the ones a writer asks: why is this function called twice, what happens when this list is empty, where is the caller that made this parameter optional. Some of those have good answers the author knows. That is fine, and it is the point. You want the questions surfaced, not pre-answered.
We have not benchmarked this and will not pretend to a number. The argument is mechanical rather than measured: the reviewer's value comes entirely from what it does not know, so anything that leaks the author's reasoning into the reviewer reduces the value. Which gives you the one design rule that matters more than every other decision in this post.
A different model helps on top of a different session, for a separate reason. Models have correlated blind spots within a family and less correlated ones across families. If the author and the reviewer are the same model, the reviewer is blind to roughly the class of thing the author was blind to. Using a different vendor breaks that correlation, which is also the practical argument for keeping more than one CLI installed. If you only have one, use it anyway in a fresh session. Most of the benefit is the reset.
why this is the cheapest quality step available
Review is cheap for a reason worth understanding, because understanding it tells you where else the same trick applies.
Writing code is expensive in the way that costs most. The author reads a large amount of the repository, holds it, and then writes a large amount of output: implementation, tests, explanations, revisions after each thing it tried did not compile. Reviewing is the opposite shape. The reviewer reads one diff and some context, and writes a short list. Its output is measured in a page of text.
On every published rate card we have read, output tokens are priced several times higher than input tokens. So a step whose output is a page is structurally cheap regardless of which vendor you use, and it stays cheap when prices move, because the ratio is the durable part. The exact arithmetic, with the cache mechanics that dominate a real session, is in what agents cost per month and the cost of a context window. Both of those are measurement posts and own their numbers; this one deliberately does not.
One honest caveat on the cheapness. If you are on a subscription rather than paying per token, the reviewer is not free, it consumes the same usage window as everything else. On a long day with several agents running, a reviewer on every commit is a real fraction of your allowance, and the way you find out is a rate limit at the wrong moment. That is the same argument as setting a token budget: the cost you cannot see is the one that surprises you.
two ways to wire it
As a hook
The hook version is automatic and unmissable, which is its whole advantage. You do not decide to run the reviewer, and therefore you cannot skip it on the afternoon when you are tired and it would have mattered most.
The natural attachment point is Stop, which fires when the agent has finished responding. The hook takes the diff from the working directory, sends it to a second model non-interactively, and writes the result somewhere you will look. The full mechanics of hook configuration, exit codes and payloads are in the hooks guide; the part that matters here is the shape:
#!/bin/sh
# .claude/hooks/review.sh - registered on Stop.
set +e
diff=$(git diff HEAD 2>/dev/null)
[ -n "$diff" ] || exit 0
# Fresh session, no memory of how the code got written.
printf '%s' "$diff" | claude -p "$(cat "$CLAUDE_PROJECT_DIR/.claude/review-prompt.md")" \
> "$CLAUDE_PROJECT_DIR/.review.md" 2>/dev/null
exit 0Note the two guards and the exit 0 at the end. A reviewer hook that exits non-zero on a blocking event can stop the agent it is supposed to be checking, and a review step is emphatically not worth wedging a session over. The hooks guide has the general form of that rule; this is one instance of it.
A tempting variant is to make the review blocking: refuse to let the turn end until the reviewer is happy. Do not, at least not at first. You have built a loop between two models with no human in it, and its failure mode is expensive in tokens and invisible until you read the bill. If you want the reviewer to be able to send work back, put a cap on the number of rounds and read the transcript for the first week.
The failure this prevents: reviewing only the diffs you suspected were risky. Those are not the diffs where the bug is; the bug is in the one that looked routine.
As a second agent
The second-agent version is a terminal, in a directory, with a person deciding when to run it. It is less automatic and considerably more flexible, because you can talk back to it.
# in a directory that is not the author's worktree
cd ~/code/myapp
git diff main...feat/checkout > /tmp/review.diff
codex "Read /tmp/review.diff. You did not write it and have no context on
why it was written this way. Review it against the rules in REVIEW.md."Two details make this work rather than merely happen. Run the reviewer in a different directory from the author, ideally your main checkout rather than the agent's worktree, so it cannot accidentally edit the code it is reviewing and so a stray build does not fight the author's. And give it a branch diff, not a working-tree diff: main...feat/checkout is the whole piece of work, which is the unit a human would review, whereas git diff is whatever happens to be uncommitted right now.
The reviewer should not be allowed to fix what it finds. It is not being asked to write code, it is being asked to produce a list you decide on. A reviewer that also edits is an author, and now nobody has read that diff.
what goes in the reviewer's prompt
This is where the technique lives or dies, and it is worth committing the prompt to the repository as a file rather than retyping it. A committed REVIEW.md can be improved every time a reviewer misses something, which is how it gets good, and it applies to every agent that reads it.
Five things it must say
- That the reviewer did not write this. One sentence, stated plainly: you have no context on why these choices were made, do not assume there was a good reason. Without it, a model asked to review will manufacture a charitable justification for anything odd, because that is what reading code usually requires.
- What this repository's rules actually are. Not general good practice. The specific things that are true here: errors are returned not thrown, this directory is generated, that module is deprecated, migrations are append-only. A reviewer without these reports style opinions, because style opinions are all it has.
- The severity ladder, with what belongs on each rung. Correctness bug, security issue, data loss, performance cliff, missing test, style. Naming the ladder is what lets you ask for the top of it.
- The output format, exactly. File, line, severity, one sentence on the consequence, one sentence on the fix. A format with a consequence field forces the model to say why the finding matters, and findings that cannot fill that field are the ones you wanted dropped.
- Permission to say nothing. An explicit line: if the diff is fine, say "no findings" and stop. Models are strongly disposed to produce output when asked for output, and this is the sentence that gives an empty answer a name.
# REVIEW.md
You are reviewing a diff you did not write. You have no context on why any
choice was made. Do not assume there was a good reason.
## This repository
- Errors are returned, never thrown, outside of src/bin/.
- src/db/schema.generated.ts is generated. Never suggest editing it.
- Anything under migrations/ that already exists has been applied and is history.
- All money is integer minor units. A float touching a currency is a bug.
## Report only these, in this order
1. Correctness: it does not do what the surrounding code implies it does.
2. Security: injection, secrets in source, authorisation missing on a new path.
3. Data loss: a delete, a truncate, a migration that drops or rewrites.
4. Concurrency: a race, an unawaited promise, a lock held across an await.
5. A new code path with no test.
## Do not report
Formatting, naming, import order, comment style, "consider extracting",
"could be more idiomatic", or anything the formatter or linter already owns.
## Output
At most five findings, most severe first. Each one:
file:line - SEVERITY - what breaks, in one sentence
- the smallest fix, in one sentence
If nothing meets the bar above, output exactly: no findingsThe failure this prevents: a review that is technically correct and operationally useless, because it spent its attention on things a linter already handles and never got to the unawaited promise.
stopping the forty nitpicks
The first reviewer anyone builds returns forty findings, of which two matter. This is not a model failure, it is a specification failure: asked to review, with no bar and no budget, a model reports everything it noticed, and it noticed everything.
Four things fix it, and they compound.
- Cap the count. "At most five findings, most severe first." The cap does the work indirectly: it forces a ranking, and ranking is the step that discards the nitpicks. Asking for "only important findings" does not work, because important is not a bar the model can apply without a comparison.
- Write the exclusion list. Naming what not to report is more effective than describing what to report, because it is checkable. "Do not report formatting" is a rule with an obvious violation; "report significant issues" is not.
- Require a consequence. Every finding must say what breaks. A nitpick cannot fill that field without becoming visibly silly, and the model will drop it rather than write "consequence: the import order is unconventional".
- Delete the categories your tooling already owns. If the formatter runs on every edit, formatting cannot be a finding. If the type checker runs in CI, type errors cannot be a finding. Every category you can move to a deterministic tool is a category the reviewer's attention no longer spends, and deterministic tools are better at those categories anyway.
There is a fifth, subtler one. Give it a smaller diff. Review quality falls off with diff size for the same reason human review quality does: a thousand-line diff gets skimmed, by anybody. If your agents are producing diffs too large to review properly, the reviewer is not the thing to fix. That is a signal about how the work was split, which is the subject of running agents in parallel.
what it reliably misses
This is the section that decides whether you use the technique correctly, so it is the one worth reading twice. A reviewer that sees a diff can only reason about the diff. Everything below is a category it will miss consistently, not occasionally, and no prompt fixes any of them.
There is also a failure mode of the reviewer itself, and it is the one to watch for in the first fortnight: confident wrongness about the code it was not shown. Asked to review a diff that calls a function defined elsewhere, a reviewer will sometimes reason about what that function probably does and report a bug in the interaction. The reasoning is fluent, the finding is specific, and it is about a function that does not behave that way. Give the reviewer read access to the repository if you can, which reduces this substantially, and treat any finding about code outside the diff as a question rather than a fact.
The correct summary is narrower than the pitch. A second model is a good check on the code and no check at all on the decision. It replaces the read-through where you would have caught the unawaited promise. It does not replace you deciding whether this was the right change, which is the part that was always yours and remains the ceiling on how many agents one person can actually run.
where it sits in a day
The order that works, and the reason for the order, is that each step is cheaper than the one after it and therefore should fail first.
- Formatter and linter, on every edit. Deterministic, instant, free. Runs as a
PostToolUsehook so the model never spends a token on it. - Type checker and tests, before the review. If the branch does not compile, the reviewer is reading code that cannot run, and it will spend its five findings on consequences of the thing the compiler already told you.
- The second model, on the branch diff. Once per piece of work, not once per commit. A piece of work is the unit a human reviews and it is the unit that has a coherent intent to check against.
- You, reading the diff. Last, and not optional. The reviewer's output is a list of places to look first, which is genuinely valuable and is not the same thing as having looked.
One habit worth adopting: read the reviewer's findings before you read the diff, then read the diff anyway. Findings first tells you where the attention should go. Reading the diff anyway is what stops the reviewer's silence from becoming your approval, which is the way this technique quietly stops working after about a month of it being right.
common questions
Can AI review its own code?
Badly. An agent asked to review a diff it just wrote is being asked to contradict its own reasoning while that reasoning is still in its context, and it mostly agrees with itself. A separate session that sees only the diff and not the conversation that produced it gives usefully different answers, and a different model gives more different answers again. The practical rule is that anything which leaks the author's reasoning into the reviewer, including pasting the transcript, reduces the value of the review.
Why is AI code review cheap compared to writing code?
Because review is nearly all input tokens and almost no output tokens. The reviewer reads a diff and writes a short list, where the author read much of the repository and wrote a large amount of code, tests and explanation. On every published rate card we have read, output tokens are priced several times higher than input, so a step that produces few output tokens is inherently cheap. On a subscription rather than per-token billing it is not free, though: it draws on the same usage window as everything else.
Should the reviewer be a different model from the author?
A different session is what matters most, and a different model helps on top of that. The gain from a fresh session is that the reviewer has no memory of the reasoning that produced the code, so it asks a reader's questions rather than re-reading a writer's justification. The gain from a different model is that its blind spots are not correlated with the author model's. If you only have one CLI installed, use it in a fresh session anyway; most of the benefit is the reset.
How do I stop an AI reviewer producing forty nitpicks?
Cap the output count in the prompt, give it a written list of what not to comment on, and require every finding to carry a file, a line and a concrete consequence. A reviewer told to return at most five findings ranked by severity has to rank, and ranking is the step that discards the nitpicks. Move every category your formatter, linter or type checker already owns out of the reviewer's scope entirely, because deterministic tools are better at those categories and cheaper.
What does an AI code reviewer miss?
Anything requiring knowledge outside the diff. It cannot tell you the feature was not what the customer asked for, that the code duplicates something in a file it was not shown, that a migration will lock a table because it does not know the table size, or that a change is fine in isolation and wrong in combination with a change on another branch. It also struggles with absence: a missing error path or an unwritten test is much harder to notice than a wrong line. It is a check on the code, not a check on the decision.
one implementation
We build a macOS app called Skribbl that puts agents and their terminals on one canvas, and the reviewer pattern is the one it was shaped around: a second agent, in its own node, that can be handed work by the first one only if you have drawn a line between them. Cutting the line revokes it. That is our answer to the authority question rather than a claim that it is the only one, and it is described honestly against the alternatives on the comparison page.
None of this post needs it. A reviewer is a prompt file, a diff and a second terminal, and every command above runs on a plain machine. If you want the wiring rather than the argument, the hooks guide has the hook mechanics in full, the docs describe the app, and the download page has the build. If you are running one agent at a time, a second model on the diff is still the first upgrade worth making, and it costs nothing to try on the next branch you finish.