Skip to content

GUIDES

AI Code Review Workflow: Gate on Low, Advise on High

An ai code review workflow that blocks on findings stopped our correct change 4 times in 4. At low effort it caught every planted bug and blocked nothing.

An ai code review workflow should block merges on a low-effort review and post the high-effort one as advice. We ran Claude Code's /code-review sixteen times against one fixture: a diff with four planted defects and a correct change of similar size. At high effort the correct change was blocked 4 times in 4, drawing more findings than the broken one. At low effort every planted defect was caught and the correct change passed. By the end you will have a gate script, a pre-push hook, and the two bugs our own first gate shipped. Measured 2026-10-01 on Claude Code 2.1.286, Sonnet. If you are still picking the tool, the assistants pillar comes first.

Key takeaways

  • A high-effort review cannot be a merge gate. It blocked a correct 7-line change on every run, with a mean of 7.0 findings against 5.4 for the diff carrying four real bugs.
  • A low-effort review can. It found all four planted defects in both runs and replied (none) to the correct change, at $0.048 and 13 seconds against $0.076 and 23 seconds.
  • Detection was never the problem. All four defects were found in all seven defect runs, at both levels and in every checkout shape.
  • Our first gate failed open. It diffed against HEAD, read zero lines on a committed branch, and passed the broken one without calling the model.
  • A clean review has its own reply shape. It is the literal (none), and a parser that does not expect it fails a correct change.

The short answer

Split the review into a gate and an advisor. The gate runs /code-review low, blocks on any finding, and fails closed when the reply cannot be read. The advisor runs at high, posts its findings where a human will triage them, and never blocks.

The reason is measured, not stylistic. A high-effort review reports pre-existing code, missing input validation and "could be a single reduce" alongside real defects, and it does that on correct changes too. What the review command catches was already established on this site; what a workflow needs to know is what a gate built on it decides.

What we measured

One fixture, committed at scripts/fixtures/review-gate-project/: a 3-file billing module with a README that states the contracts. Two variants of the same change sit on top of it.

VariantDiff linesWhat it contains
defects11Four planted bugs, no comments, plus one new helper
clean7The same new helper and a harmless refactor

The four planted defects each break a sentence in the README or a language rule: page offset page * size against 1-based pages, <= against "strictly before today", a dropped await on mailer.send, and a removed guard on the optional customer.contact. The corpus carries no comments, because the first review corpus on this site labelled its defects and one run quoted the label back.

Every arm builds a throwaway git repository and runs:

Terminal
claude -p "/code-review high" --model sonnet --permission-mode bypassPermissions --output-format json

Thirteen review arms, three billed end-to-end gate runs and one free one, $1.05 in total. Every reply is recorded in scripts/fixtures/review-gate.json.

The correct change drew more findings

At high effort, the review blocked the correct change on all four runs. It found 6, 8, 6 and 8 issues. The defects diff, with four real bugs in it, drew 6, 6, 5, 5 and 5.

Variant at highRunsFindings per runPlanted defects found
defects55–6, mean 5.44 of 4, every run
clean46–8, mean 7.0none planted

The clean findings are not wrong in the way a hallucination is wrong. They are true statements about code nobody asked about: listInvoices does not validate page, isOverdue assumes both dates share a type, the extracted subject variable "is a no-op refactor". One run labelled two of its own findings Pre-existing: and reported them anyway.

That is the right behaviour for a reviewer and the wrong behaviour for a gate. A gate that blocks on any high-effort finding blocks every change, and a team that hits that twice learns to override it.

Low effort is the gate

At low effort the separation was complete on this fixture: the defects were all found, and the correct change produced nothing.

LevelVariantRunsResultMean costMean time
lowdefects2block, 4 of 4 found$0.04913 s
lowclean2(none), pass$0.04612 s
highboth9block, every run$0.07623 s

The low reply is shorter and a little looser. One run folded the dropped await and the removed guard into a single finding on line 7, so it reported three findings that covered four defects. For a gate, that does not matter: one finding blocks.

The gate that passed a broken branch

Our first gate counted changed lines with git diff --numstat HEAD, and skipped the model when the count was zero. On a developer's machine with uncommitted edits, that works. On a CI checkout, or in a pre-push hook, the change is already committed, the working tree is clean, and the count is zero.

The gate printed pass — empty diff, model not called for the branch with four bugs in it. The CLI was never involved, so nothing in its exit code could have warned us. The check re-derives this live on every run:

Our verification run
npm run check:review-gate
# → live: committed checkout reads HEAD=0, merge-base defects=11 clean=7

The fix is to diff from the merge base with the target branch to the working tree, which covers committed and uncommitted work in one number. Both arms where the defects were committed now block.

An ai code review workflow in three stages

This is the ai code review workflow the measurements support, in the order a change meets it.

  • Stage 1, the gate. /code-review low on the branch, before it leaves the machine. Block on any finding. Fail closed on an unreadable reply or a cost over the ceiling.
  • Stage 2, the advisor. /code-review high on the pull request, findings posted as comments. Never required.
  • Stage 3, the human. Triage the advisor's findings and read the diff. Hardest on long agent runs that nobody watched being written.

Deterministic checks sit beside Stage 1, not behind it. A test that pins the 1-based page contract catches the page-offset defect for free every time. The review is for the defects nobody wrote a test for. Our workflow audit of this repository found zero automated triggers, and this gate is the first one.

How to automate code review before a push

The gate is scripts/review-gate.mjs. Its whole decision is one pure function, so the check replays it over every recorded reply without spending a turn:

scripts/review-gate.mjs (excerpt)
export function decide(envelope, { diffLines, maxUsd }) {
  if (diffLines === 0) return { verdict: "pass", reason: "empty diff, model not called" };
  if (!envelope || envelope.is_error) return { verdict: "error", reason: "review did not complete" };
  if (envelope.total_cost_usd > maxUsd) return { verdict: "error", reason: `cost ${envelope.total_cost_usd} over ceiling ${maxUsd}` };
  const parsed = parseFindings(envelope.result ?? "");
  if (parsed.findings.length) return { verdict: "block", reason: `${parsed.findings.length} finding(s)`, ...parsed };
  // …a literal (none) or "no issues" is a pass; anything else is an error
}

To automate code review before a push, call it from a hook:

.git/hooks/pre-push
#!/bin/sh
# Block the push when the low-effort review reports a finding or cannot be read.
node scripts/review-gate.mjs --base origin/main --level low --max-usd 0.25

We pushed the defects branch to a local bare remote with that hook installed. The gate reported four findings in 15 seconds for $0.078, git printed failed to push some refs, and the remote still held only main. On the clean branch the same gate passed in 11 seconds. On main with nothing changed it passed at $0, without calling the model.

Where ai pr review belongs

The advisory review belongs on the pull request, where a human reads it next to the diff. /code-review accepts --comment to post findings as inline pull request comments, and the GitHub integration surfaces cover where that runs. Anthropic's GitHub Actions documentation covers the hosted route.

Do not use the same run for both jobs. An ai pr review at high effort is worth reading for the six things it notices. It is not worth blocking on, for the same reason. The CLI reference documents --output-format json, which is what makes the gate's cost ceiling possible: total_cost_usd arrives in the envelope next to the result.

What did not work

The first gate failed open on every committed branch. Covered above. It is the most expensive kind of bug a gate can have, because it looks like success.

The first gate also failed a correct change. A clean low-effort review replies (none), on its own line. The first parser recognised a JSON array or the words "no issues", treated (none) as unreadable, and returned error on the correct change. Both bugs were found by replaying recorded replies, and both fixes carry a comment in the script.

Account notices arrive inside the result text. One reply carried a trailing paragraph about MCP connectors that needed authorisation, unrelated to the review. Our line parser ignored it because it has no file:line shape. A parser that counted paragraphs as findings would have blocked on it. It was removed from the recorded text and the arm is marked in the fixture.

Best practices

  • Gate at low, advise at high. The measured separation is on low; high blocked a correct change every time.
  • Diff from the merge base. HEAD is zero on every checkout that matters for a gate.
  • Fail closed. A non-empty diff with an unreadable reply is an error, not a pass.
  • Put a cost ceiling in the gate. The envelope reports total_cost_usd, so a runaway review becomes a failed check rather than a bill.
  • Skip the model on an empty diff. It is free and it is the common case on main.
  • Replay recorded replies when you change the parser. Both of our bugs were caught that way, and neither cost a turn.

Common mistakes

  • Gating on the CLI's exit code. It is 0 whether the review found six bugs, found nothing, or could not see the diff. Parse the reply.
  • Counting findings as quality. The correct change drew more findings than the broken one. A count measures the reviewer's thoroughness, not the change.
  • Testing the gate only on broken code. Every gate catches planted bugs. Ours was only shown to be unusable at high effort when we ran it on a correct change.
  • Making the advisory review required. A required check that blocks correct work teaches people to bypass checks.

What we are not claiming

This is one fixture: three files, four planted defects, one correct change, one model (Sonnet) on one machine. It does not show that low effort separates real from noise on a large codebase, on defects that need cross-file context, or on a different model. Diff-only review has a structural ceiling that no effort level raises. We make no claim about any other review product.

Conclusion

An ai code review workflow starts with the gate, not the bot. Run /code-review low from a pre-push hook or a required CI step, diff from the merge base, and fail closed on anything you cannot parse. Post the high-effort review on the pull request and let a human decide what it is worth.

We replay all thirteen recorded review replies against the shipped policy with an internal check script. Before you require the gate, run the clean arm on five of your own recently merged changes. If the low review blocks any of them, it is not ready to be required on your repository.

Frequently asked questions

Should an AI code review block a merge?
Only at a low effort level, and only with a parser that fails closed. On our fixture a high-effort review blocked a correct change four times in four, with six to eight findings each run. A low-effort review caught all four planted defects in both runs and returned no findings on the correct change. Block on low; post high as comments.
How do I automate code review with Claude Code?
Run claude -p with the /code-review command and --output-format json, parse the findings from the result text, and exit non-zero when there are any. Measure the diff from the merge base, not HEAD, and treat a non-empty diff with an unreadable reply as a failure. Our gate script does this in about 70 lines and runs from a pre-push hook.
Why did my AI review gate pass a branch with bugs?
Check what the gate diffs against. Our first version counted changed lines against HEAD, which is zero on any checkout where the change is already committed, so it skipped the review and passed. The fix is git merge-base with your target branch, then git diff from that commit to the working tree, which covers committed and uncommitted work.
How much does an AI pr review cost per run?
On an 11-line diff with Claude Code 2.1.286 on Sonnet, a low-effort review averaged $0.048 and 13 seconds, and a high-effort review averaged $0.076 and 23 seconds, as of October 2026. Cost scales with the diff and the effort level. An empty diff should cost nothing, because a well-built gate never calls the model.
What does a clean AI code review reply look like?
At low effort, Claude Code's /code-review in print mode replied with the literal text (none) on both clean runs. There was no empty JSON array and no sentence. A gate that only recognises JSON or the phrase no issues will read that reply as unparseable and fail a correct change, which is exactly what our first version did.

Muhammad Kashif

Founder and editor of Devventa, covering AI coding assistants, Next.js and the modern AI development stack.