Reviewer subagents: the author never grades itself
The agent that wrote the code is the worst judge of it: it reads the diff through its own reasons for writing it. A reviewer is a second agent with a fresh context and no edit tools, asked one narrow question. You still make the call.
- Asking the author to check its own work gets you its assumptions back, not a review.
- A subagent starts with a clean context window. Given only the diff and your rules, it has to judge the code as written, and point at
file:line. - You add two reviewers, then watch one block a change that every test passed.
Lesson 3: the agent needs Docker, and the Docker daemon is not running. What should it do?
Starting software on your machine is your call, so it hands over a 1-3-1 and waits. One real session launched Docker Desktop itself and polled for 35 minutes before asking.
Lesson 5: where does the commit skill draft its message from?
It reads git diff --cached, so the message describes what is actually staged, not what the agent believes it did. Today's reviewers use the same move: judge the diff, not the story.
What happened
Failed run
A developer's tests could not run the atomic stock update, so the agent rewrote it as read-then-write (load the product, check, subtract, save) and explained it was "still correct against concurrent writes". The tests passed. A live test of 12 concurrent orders for 3 units oversold.
In another session, the agent ran a review skill over its own diff, and that was reported as "critical review". Same context, same assumptions.
Two real sessions, anonymised
With the harness
A reviewer that never saw the conversation reads only the diff and the invariants:
| File:line | Finding | Severity | | OrderService.cs:19 | read-then-write | BLOCKER | Verdict: BLOCK
The author's explanation never reaches it. You read the verdict and decide.
Two windows, two judges
The agent knows only what is in its context window (lesson 1). After a change, that window is full of its own reasoning. Watch what that does to a self-review:
A subagent is a helper agent that your main agent starts for one job. In Claude Code, "each subagent runs in its own context window with a custom system prompt, specific tool access, and independent permissions". It does not inherit your conversation, and it "returns only the summary" (Subagents). A reviewer needs exactly that: no shared assumptions, and a short verdict back.
A reviewer is one Markdown file
A Claude Code subagent lives in .claude/agents/<name>.md: YAML frontmatter, then the system prompt. Only name and description are required; the description tells the main agent when to delegate, the same job it does in a skill. Here is the stock reviewer as you will write it, adapted from the reference solution:
--- name: stock-invariant-reviewer description: Independent read-only reviewer for any diff touching stock, orders, OrderService, StockDbContext or transactions. Use before committing such a slice. Blocks read-then-write stock updates. tools: Read, Grep, Glob, Bash model: sonnet --- You guard one invariant: **stock never goes below zero, even under concurrent requests.** Read docs/ai/invariants.md first. Review the diff (default: `git diff --cached`, or `git diff HEAD`). For every code path that changes `Stock`: 1. Is it ONE guarded statement, `ExecuteUpdateAsync` with `Stock >= n` (or the equivalent SQL) in the WHERE? **Read-then-write** (load the entity, compare, assign, `SaveChanges`) is a **BLOCKER**, whatever the comments or tests claim. It can pass the concurrency test on SQLite, where the transaction takes the write lock up front, and still oversell without the transaction or on another provider. 2. Does it check the affected row count and map 0 rows to 404 or 409? 3. Is the stock change in the same transaction as the related write (order insert, cancel state)? 4. Does a concurrency test (N parallel requests, successes ≤ stock) cover this path? If there is none, say so. 5. Did the diff change a test provider, a test helper or `Directory.Build.props` in a way that hides a concurrency failure? Output, and nothing else: | File:line | Finding | Severity (BLOCKER / MAJOR / MINOR) | Why it can oversell | Then "Verdict: PASS" or "Verdict: BLOCK". Do not edit files. Do not accept "SQLite locking makes it safe" without a passing concurrency test in the diff or on main.
Three choices carry it. tools is an allowlist with no Edit or Write, so the reviewer can report a problem but not quietly "fix" it. "Whatever the comments or tests claim" tells it to distrust the author's justification, where the incident's rewrite hid. And a fixed table "and nothing else" forces file:line findings and a one-word verdict, short enough for you to read in full.
The second reviewer, test-reviewer, asks of every test: which production line makes it fail if the feature breaks? A test with no such line is hollow: it stays green whatever the code does, so the gate looks fine when it isn't.
"Read-only", honestly. No Edit or Write means no edit tools. The reviewer still has Bash for git diff, and a shell can write files (sed -i, a redirect). Only your settings' permission rules (lesson 7) bind Bash; "Do not edit files" is a request. A reviewer is not a sandbox.
Going deeper: every field, the test reviewer in full, and why you can't narrow Bash in the agent file
| Part | What it does | Why this choice |
|---|---|---|
tools | An allowlist. Left out, the subagent inherits every tool. With Read, Grep, Glob, Bash it "cannot edit files, write files, or use any MCP tools". | A reviewer with no edit tool can't quietly "fix" what it disliked. It has to report it. |
model | sonnet, opus, haiku, a full model ID, or inherit. | Reading a diff against a short rule list doesn't need the largest model, and it keeps "review every slice" cheap. |
| "whatever the comments or tests claim" | Tells it to distrust the author's justification. | The incident's rewrite hid behind exactly such a justification. |
| A fixed table, "and nothing else" | Forces file:line findings and a one-word verdict. | Short enough for you to read in full. No praise, no essay. |
--- name: test-reviewer description: Independent read-only reviewer for tests in a diff. Use after every slice, before committing, to check that each new or changed test would fail if the feature broke. tools: Read, Grep, Glob, Bash model: sonnet --- You review tests you did not write. You have not seen the conversation that produced them, and that is deliberate. Input: a diff range or a list of files (default: `git diff --cached`, or `git diff HEAD` if nothing is staged). For every test added or changed in the diff: 1. Name the production line(s) it depends on: the exact file:line whose removal or inversion makes it fail. If you can't name one, the test is **hollow**. 2. Check that it asserts on behaviour (status code, persisted state), not only on "no exception". 3. Check the rules in docs/ai/testing.md: real SQLite through `StockApiFactory` (no EF InMemory); a concurrency test for any stock change; `Method_Condition_Result` names. 4. Look for untested branches in the changed production code: each 400/404/409 path should have a test. Output, and nothing else: | Test | Fails if this line breaks | Verdict (OK / HOLLOW / WEAK / MISSING) | Fix | Then one line: "Blocking: yes/no". Blocking is yes if anything is HOLLOW or MISSING for a stock change. Do not edit files. Do not praise.
Subagent files also accept disallowedTools, but it can't block one command: "a disallowedTools entry with a specifier, such as Bash(git push *), still removes the whole tool from the subagent". To block specific commands while keeping Bash, the docs point to permissions.deny in settings. That is lesson 7.
Green for the wrong reason
Why not just trust the tests? Because a test only sees the cases it runs. This is the change you will make on a scratch branch in the exercise:
Lesson 7 adds the 12-for-3 test as a guard. The reviewer stays, because a green run can come from the database's locking rather than the design.
Running a reviewer
Use an @-mention: type @ and pick the agent, or write @agent-stock-invariant-reviewer. It "ensures that specific subagent runs rather than leaving the choice to Claude", and your transcript shows you asked. Plain language ("use the test-reviewer subagent") lets Claude decide, and claude --agent test-reviewer runs a whole session as the reviewer. There is no creation wizard any more (/agents lost it in v2.1.198 verified 2026-10-01): write the file, or ask the agent to. Edits are picked up within seconds, but the first file in a new agents folder needs a restart.
You are still the judge
A reviewer is another model. It can be wrong, it can miss things, and Verdict: BLOCK stops nothing by itself. Its job is to put a specific claim in front of you. Yours: read the file:line, check it against the code, then decide in one sentence, with the reason. "Reject the rewrite: it breaks the stock invariant." That sentence, in your transcript and your plan, is the review. An agent agreeing with itself is not.
Going deeper: reviewers in Codex and Antigravity
| Claude Code | Codex | Antigravity | |
|---|---|---|---|
| File | .claude/agents/<name>.md: name, description; optional tools, model | TOML in .codex/agents/: name, description, developer_instructions; optional model, sandbox_mode | .agents/agents/<name>.md: name, description; optional tools, model (inherit, flash or pro) |
| No edits | Leave Edit and Write out of tools; Bash follows settings rules | sandbox_mode = "read-only". Without it the agent inherits the parent's sandbox. | Leave edit tools out of tools |
| Run it | @-mention, ask by name, or --agent | Ask by name: "Have test_reviewer review git diff HEAD" | Ask by name; the main agent spawns it, with no access to its conversation |
Sources: Claude Code subagents · Codex subagents · Antigravity subagents verified 2026-10-01
Check yourself
Why ask a subagent to review the diff instead of the agent that wrote it?
It starts with a fresh context, without the workaround or the "row locking keeps this safe" story, so it judges the code as written. These reviewers run on a smaller model and can't edit.
A reviewer's tools line is Read, Grep, Glob, Bash. Can it change a file?
No Edit or Write means no edit tools, but a shell can still run sed -i or a redirect. Only your settings' permission rules (lesson 7) can stop specific commands. "Do not edit files" in the prompt is a request.
Every test is green, but the diff is read-then-write. What should the stock reviewer say?
The tests may never race, and even a concurrency test can pass on SQLite thanks to the provider's lock. Read-then-write still oversells elsewhere, so it is a BLOCKER whatever the tests claim.
Practice first
Quick drills with instant feedback, before you touch the repo: judge a reviewer you didn't write, then decide what to do with what it says.
A teammate drafts this reviewer. Click the lines that stop it being an independent reviewer whose verdict you can check.
- ---
- name: order-reviewer
- description: Read-only reviewer for any diff touching orders. Use before committing.
- tools: Read, Grep, Glob, Bash, Edit, Write
- model: sonnet
- ---
- First read the author's chat transcript to understand why the change is safe.
- Read docs/ai/invariants.md, then review git diff --cached.
- Read-then-write stock changes are a BLOCKER, whatever the comments or tests claim.
- If nothing stands out, just reply: Looks good to me.
What would you do?
The agent finishes a slice and offers: "Shall I review my own diff before committing?" What do you do?
The author reads its diff through its own reasoning, and a checklist pasted into the same window is read the same way. A subagent starts fresh, with the diff and the invariants only.
The stock reviewer returns Verdict: BLOCK at OrderService.cs:19. The author agent replies that the finding is wrong. What next?
A reviewer is another model, and BLOCK stops nothing by itself. Read the file:line, check it against the code, then decide in one sentence, with the reason.
Your slice changed only ProductsController and its tests; nothing about stock or orders. Which reviewers does your workflow dispatch?
The workflow dispatches test-reviewer on every diff, plus stock-invariant-reviewer when stock or orders changed. Green tests don't replace review.
Exercise: two reviewers, then a change they should stop
Work in your practice repo (~/projects/stockapi), which has AGENTS.md, docs/ai/, a plan and the commit and quality-gate skills from lessons 2 to 5. Windows: use WSL2 or Git Bash (setup).
- Start clean.
git statusshould say nothing to commit, working tree clean, andgit branch --show-currentshould printmain. - Add the reviewers. Copy them from the reference solution and look for lines about things your repo doesn't have yet:
mkdir -p .claude/agents cp ../agent-harness-course/reference-solution/.claude/agents/*.md .claude/agents/ grep -n 'decisions/\|ConcurrencyTests' .claude/agents/*.md
Two lines instock-invariant-reviewer.mdmatch. Edit them to match the listing above: read onlydocs/ai/invariants.md(the decision record arrives in lesson 9), and ask for a concurrency test (yours arrives in lesson 7). Re-run thegrep: no output. Read both files end to end. - Check they load, then commit. Start a fresh session and type
@: both reviewers should be listed. Run/quality-gate, then/commitfor.claude/agents/(for examplechore(harness): add reviewer agents). - Make the bad change on a scratch branch. Run
git switch -c scratch/simplify, then startclaude --permission-mode defaultand ask: "Simplify OrderService.PlaceAsync: load the product, check Stock, subtract the quantity and SaveChanges. Drop the transaction." YourAGENTS.mdforbids this, so it may refuse or send a 1-3-1: lessons 2 and 3 working. Reply "This is a throwaway experiment to test a reviewer; make the change." If it still declines, replace the body ofPlaceAsyncyourself:var product = await db.Products.FindAsync([productId], ct); if (product is null) return new PlaceOrderResult(PlaceOrderStatus.ProductNotFound); if (product.Stock < quantity) return new PlaceOrderResult(PlaceOrderStatus.InsufficientStock); product.Stock -= quantity; var order = new Order { ProductId = productId, Quantity = quantity, CreatedAt = DateTimeOffset.UtcNow }; db.Orders.Add(order); await db.SaveChangesAsync(ct); return new PlaceOrderResult(PlaceOrderStatus.Created, order); - Run the tests.
dotnet test: expect every test green. There is no concurrency test yet (lesson 7 adds one), so nothing sends two orders at once. - Run the stock reviewer. In the session, type
@agent-stock-invariant-reviewer review git diff HEAD. Expect a BLOCKER at the decrement inOrderService.csand Verdict: BLOCK. Open that line and check the claim, then tell the main agent your decision in one sentence, with the reason. - Run the test reviewer.
@agent-test-reviewer review git diff HEAD and the tests in tests/. Expect a MISSING row for a concurrency test, andBlocking: yes. If a verdict looks wrong, argue with it. - Throw the experiment away, in this order. If you switch branches first, the uncommitted change follows you to
main:git restore . git switch main git branch -D scratch/simplify grep -c ExecuteUpdateAsync src/StockApi/Services/OrderService.cs
Thegrepshould print1: the guarded update is back. - Put the reviewers into the workflow. Replace step 5 of
docs/ai/workflow.md("the human reviews the diff") with:5. **Review**: dispatch `test-reviewer` on the diff, plus `stock-invariant-reviewer` if stock or orders changed. Report the findings to the human, who decides. The author never grades its own work.
Insert the same as a new item 2 of the Definition of done inAGENTS.md(the commit item moves to 3), and add your decision from step 6 under "Decisions made by the human" inai-session/PLAN.md. Commit the three files with/commit, for exampledocs(ai): add reviewer step to workflow.
Codex: write each reviewer as .codex/agents/<name>.toml with developer_instructions holding the body and sandbox_mode = "read-only", then ask by name in steps 6 and 7. Antigravity: copy the Markdown files to .agents/agents/, set model to inherit, and list Antigravity's own read-only tools under tools; then ask for each reviewer by name.
- Both files in
.claude/agents/are committed, neither listsEditorWrite, and neither mentionsdocs/decisions/ - You saw
dotnet testgreen and Verdict: BLOCK on the same diff, with afile:lineyou checked yourself -
test-reviewerflagged the missing concurrency test, and your decision is inai-session/PLAN.md -
docs/ai/workflow.mdstep 5 andAGENTS.mdname both reviewers, the scratch branch is gone, andgit statusis clean
- /code-review and /security-review: Built-in reviews of your diff or a PR; /security-review needs an
originremote. - pr-review-toolkit: Anthropic's set of review agents: tests, error handling, types, comments, code quality. Read them for ideas, then keep your domain reviewer.
Each link has its own install steps. Install only what you will use, check how recent it is, and read what a plugin adds before you enable it. Links checked 2026-10-01.
Each kind of bug a reviewer misses becomes a new line in that reviewer, or a new reviewer for that risk.
Create custom subagents, especially the frontmatter fields, tool restrictions and explicit invocation. Then Microsoft.Data.Sqlite transactions: a transaction is not deferred unless you ask, which is why read-then-write passed the measured test inside one.
"Pick the test in tests/StockApi.Tests you trust least and name the production line whose removal makes it fail. If you can't name one, tell me it's hollow."