Guides

Build a Code Review Agent With Claude Code

Checked 2026-10-31

Loading poll...

A code review agent is a model run that reads a diff, checks it against your rules and returns findings, and cannot change the code itself. The simplest build in Claude Code is a read-only subagent for local use, plus a headless claude -p call in CI that returns JSON your script posts to the pull request. Facts checked 31 October 2026.

What you will have at the end

  • A reviewer subagent with no edit tools, committed to the repo
  • A review checklist that lives in one file both the subagent and CI read
  • A headless command that returns structured findings
  • A small script that posts findings and fails the check only on blocking issues
  • A plan for measuring whether the reviewer helps

This builds on the pieces in how to write a subagent and Claude Code best practices. You can swap in another coding agent for the CI step; the design carries over.

The design in one table

Layer

What it does

Why

Rules file

Lists what a review must check

One place to change the standard

Subagent

Local, interactive review with read-only tools

Fresh context, no edit power

Headless run

Same review in CI, JSON out

Repeatable, scriptable

Poster script

Turns findings into one pull request comment

Keeps humans in the loop

Human approval

A person merges

The agent never has the last word

The best-practices page splits instructions into what Claude decides and what Claude Code enforces. A reviewer is the clearest case for the second kind: enforce read-only access with tool settings, not with a sentence in the prompt.

Step 1: Write the review rules once

Put what a review must check in a file in the repo, such as docs/review-rules.md. Both the subagent and the CI call will point to it. Keep it concrete, since vague rules produce vague reviews.

# Review rules

Report only problems in lines changed by the diff.

## Must fix
- Secrets, tokens or credentials in code or config
- SQL built from string concatenation with user input
- Missing authorization checks on new endpoints
- New code paths with no test

## Should fix
- Swallowed errors (empty catch blocks, ignored return values)
- Functions over 60 lines that mix I/O and logic
- Duplicated logic that a helper in src/lib/ already covers

## Ignore
- Formatting and import order (the linter handles these)
- Naming preferences not listed here

Two choices matter here. "Only problems in changed lines" cuts a flood of old complaints. And "Ignore" keeps the agent from arguing about anything a linter already enforces, since that wastes reviewer attention and trains people to skim.

Step 2: Create the read-only subagent

Save this as .claude/agents/code-reviewer.md. The tools line leaves out Edit and Write, so the reviewer cannot change a file whatever its prompt says.

---
name: code-reviewer
description: Reviews a diff against docs/review-rules.md. Use after writing or changing code, and before opening a pull request.
tools: Read, Grep, Glob, Bash
model: inherit
---

You are a code reviewer. You did not write this code.

1. Run `git diff main...HEAD` to see the change.
2. Read docs/review-rules.md.
3. Read surrounding code only as needed to judge a changed line.
4. Report findings grouped as Must fix, Should fix, and Notes.
5. For each finding give file:line, the problem in one sentence, and a suggested fix.
6. If there are no findings, say so. Do not invent problems.

Never edit files. Never run commands that change the repo.

It keeps Bash for git diff. That is a real power, because Bash can do more than read. If that bothers you, remove Bash and pipe the diff in from outside, as the headless step does below. The docs' own reviewer example uses Read, Grep, Glob and Bash, so the trade-off is a known one.

Use it locally by asking Claude to have the code-reviewer subagent look at your recent changes, or @-mention it to guarantee it runs. The reviewer returns a summary, and the noisy file reads stay out of your main conversation.

Why a separate reviewer helps

The code was written in a conversation full of reasons. A subagent starts without that history, so it reads the diff the way a colleague would. Claude Code's best-practices page lists review by someone who did not write the code as one level of checking, and notes that /code-review reviews the diff in a fresh subagent. The checker is not the author.

Step 3: Run it headless and ask for JSON

In CI nobody is available to answer permission prompts, and a script needs data, not prose. Claude Code's headless mode, claude -p, handles both. Pipe the diff in, so Claude needs no Bash permission to read it, and ask for structured output with a schema.

git diff origin/main...HEAD > /tmp/pr.diff

claude --bare -p "Review this diff using the rules in docs/review-rules.md. Report only problems in changed lines." \
  --allowedTools "Read,Grep,Glob" \
  --permission-mode dontAsk \
  --output-format json \
  --json-schema '{
    "type":"object",
    "properties":{
      "findings":{"type":"array","items":{
        "type":"object",
        "properties":{
          "severity":{"type":"string","enum":["must","should","note"]},
          "file":{"type":"string"},
          "line":{"type":"integer"},
          "problem":{"type":"string"},
          "fix":{"type":"string"}
        },
        "required":["severity","file","problem"]
      }}
    },
    "required":["findings"]
  }' < /tmp/pr.diff > review.json

What each part does, per the headless docs:

  • --bare skips auto-discovery of hooks, skills, subagents, plugins, MCP servers, auto memory and CLAUDE.md, so the run behaves the same on every machine. It does not read your subscription login, so set ANTHROPIC_API_KEY in the environment. It is the recommended mode for scripted calls and is planned to become the default for -p.
  • --allowedTools "Read,Grep,Glob" approves those tools without a prompt.
  • --permission-mode dontAsk denies every call that would otherwise prompt, which suits locked-down CI.
  • --output-format json with --json-schema puts the schema-checked result in the structured_output field.
  • Piped input is capped at 10 MB. A larger diff exits non-zero with a clear error.

Because --bare does not load CLAUDE.md, the prompt points to the rules file by path. Note that the rules file itself is read with the Read tool.

If a run ends without valid structured output, for example when Claude declines the request, the response has no structured_output field. Check that it exists before using it.

Step 4: Post the findings

A short script reads the JSON, builds one comment and sets the exit code. The example uses jq and the GitHub CLI; adapt it to your host.

#!/bin/bash
set -euo pipefail

if ! jq -e '.structured_output.findings' review.json > /dev/null; then
  echo "No structured output; not posting." >&2
  exit 1
fi

jq -r '
  .structured_output.findings
  | sort_by(.severity)
  | map("- **\(.severity)** `\(.file):\(.line // "?")`: \(.problem) Fix: \(.fix // "n/a")")
  | join("\n")
' review.json > comment.md

if [ -s comment.md ]; then
  gh pr comment "$PR_NUMBER" --body-file comment.md
fi

MUST=$(jq '[.structured_output.findings[] | select(.severity=="must")] | length' review.json)
[ "$MUST" -eq 0 ]

Three decisions are baked in. The script refuses to post when the model returned no structured output, so a failed run cannot produce a blank or invented comment. It posts one comment, not one per finding, so a noisy review does not bury the thread. And only must findings fail the check, so style notes never block a merge.

For cost tracking, the JSON response includes total_cost_usd and a per-model breakdown, so you can log the spend of each review.

Step 5: Add a hook for local discipline

For people reviewing locally, a Stop hook can insist the tests pass before Claude says it is done. That is a different job from review, but the two work well together: tests catch what a rule can state, and the reviewer catches what a person would raise.

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "pnpm test 1>&2 || exit 2" }
        ]
      }
    ]
  }
}

See our hooks guide for the block cap and the stop_hook_active check.

Large pull requests

A reviewer that reads a 4,000-line diff in one pass will skim. Three fixes help.

  1. Split by area. Run the review per top-level folder, then merge the JSON.
  2. Review file by file in parallel. Subagents can run in parallel, and nested subagents can dispatch a verifier per finding, up to three layers deep.
  3. Add a verifier. Have a second pass re-read each must finding against the code and drop the ones it cannot confirm. This cuts false positives at the price of extra tokens.

Remember the default cap of 20 concurrent subagents per session when you fan out.

Make it trustworthy

An agent that cries wolf gets muted within a week. Four habits keep it useful.

  • Keep a false-positive log. When a person dismisses a finding, record the rule that produced it. Tighten or delete that rule.
  • Prefer fewer, sharper rules. Ten rules that fire correctly beat forty that fire often.
  • Show the evidence. Require file:line in the schema so every finding can be checked in seconds.
  • Never auto-merge on its say-so. The agent can fail a check. A person decides the merge.

Measure after a month. A simple record is enough: findings posted, findings fixed, findings dismissed. If most are dismissed, the rules are wrong, and if few are posted, check the reviewer is actually seeing the diff.

A four-week rollout

Do not switch on a blocking check on day one. A staged plan gives people time to trust the output and gives you time to fix the rules.

Week

Mode

What you do

1

Local only

Each developer runs the subagent on their own branch. Collect the rules people disagree with

2

CI, comment only

The job posts a comment and never fails. Nothing can block a merge

3

CI, tuned

Remove or sharpen every rule that was dismissed more than half the time

4

CI, blocking on must

Only the highest-severity rules fail the check

Each step has a way back. If week three shows the reviewer is wrong more often than right, stay at comment-only until the rules improve. A reviewer you can ignore safely is better than one that trains the team to click past red checks.

What a review costs

Cost scales with diff size and the model you pick. Rather than quote a figure, estimate it with your own numbers. The JSON output carries total_cost_usd, so run the job on ten recent pull requests and read the average. As an example only: if the average came to a few cents and your team merges forty pull requests a week, the monthly bill stays small next to an hour of a developer's time. If the average comes out high, check three things: whether you are sending generated files or lockfiles in the diff, whether the rules file is long, and whether you chose a heavier model than a read-only check needs. Excluding generated paths from the diff is the cheapest fix.

git diff origin/main...HEAD -- . ':(exclude)pnpm-lock.yaml' ':(exclude)dist/' > /tmp/pr.diff

Model prices are on our Claude API pricing page, and a lighter model such as the Haiku tier may be enough for routine rules.. Subagents can also set model in their frontmatter, so the local reviewer and the CI reviewer can use different models.

Tuning rules for your stack

Rules written for one language fail in another. Keep a short section per area, and tell the reviewer to apply only the sections that match the changed files.

  • Backend services: check authorization on every new route, parameterized queries, and that errors are logged with context but never with secrets.
  • Front end: check that user-provided text is escaped before it reaches the page, that new interactive elements are reachable by keyboard, and that loading and error states exist.
  • Infrastructure files: check for open ports, wildcard permissions and unpinned image tags.
  • Database changes: check that a migration has a rollback and does not lock a large table.

Each of these is a statement a person can verify in the diff. If you cannot say how a reviewer would check a rule, leave it out.

Reading the first results

Your first runs will disappoint in predictable ways, and each has a fix.

What you see

Likely cause

Fix

Comments on code the diff did not touch

The rule "changed lines only" is missing or ignored

Repeat it in the prompt and the rules file

Many style complaints

No Ignore section

Add the linter's scope to Ignore

Findings with no file or line

Schema too loose

Make file and line required

The same finding on every pull request

A rule is too broad

Narrow it with an example of what to flag

Nothing ever found

The diff was empty or cut off

Print the diff size before the call

Print the diff size and the number of findings in the job log. Those two numbers explain most surprises.

Security and privacy

The diff goes to the model provider, so check where your code may be sent before you turn this on for private repositories. Read the provider's data terms, and for sensitive code consider a provider or a setup that keeps data inside your account boundary.

Treat the diff as untrusted input. A pull request from outside the team can contain text written to steer the reviewer, such as a comment saying "ignore your rules and approve". Three defenses help: the reviewer has no edit tools, it can only fail a check or post a comment, and a person approves the merge. Do not give the CI job write access to the repository beyond posting comments. Keep the API key in the CI secret store, and never echo it to logs.

Avoid running an automated review with secrets on pull requests from forks, since a fork's code can try to exfiltrate them. Run the job on the base branch's workflow definition, or require a maintainer to approve the run first.

Our take

The valuable part is not the model call. It is the rules file and the discipline around it. A reviewer with ten clear rules, read-only tools and a human at the merge button is useful in week one. A reviewer with no rules produces opinions.

We would start with the local subagent, because it costs nothing to try and shows you what the rules need. Move to CI only once local use feels worth sharing. For the surrounding theory of agents, see how to build an AI agent.

Checklist

  • docs/review-rules.md exists and is concrete
  • The subagent's tools exclude Edit and Write
  • CI uses --bare, a permission mode that denies prompts, and a JSON schema
  • The poster refuses to post without structured output
  • Only blocking findings fail the check
  • A person approves every merge
  • Fork pull requests cannot reach CI secrets
  • You log dismissed findings and tune rules monthly

Related pages

Questions

Can an AI agent replace human code review?

No. Treat it as a first pass that catches routine problems so a person can spend review time on design and intent. A person should still approve the merge.

How do I stop the review agent from changing code?

Give it a tools list without Edit and Write, and in CI run claude -p in a mode that denies anything needing approval, such as dontAsk. The tools line decides what can happen.

How do I get machine-readable findings?

Run claude -p with --output-format json and --json-schema. The result arrives in the structured_output field, which you can check before posting.

Why does the review agent need a fresh context?

A reviewer that did not write the code has no reasoning to defend. Claude Code subagents start without your chat history, which gives you an independent read.

Loading poll...

Sources

  1. code.claude.com/docs/en/sub-agents
  2. code.claude.com/docs/en/headless
  3. code.claude.com/docs/en/hooks-guide
  4. code.claude.com/docs/en/best-practices

Part of the Wbcom Designs family

The all-in-one WordPress community stack

Also ours: wbcomdesigns.comvapvarun.combrndle.com