Dev.to AI 🤖 Ai 👁 0 📖 12 min read

Half the Price, Missed the Production Bug: Opus vs Sonnet as Code Reviewers

Sonnet 5.5 reviewed four codebases for half of what Opus 5.5 cost and did not report a single false finding. It also missed the one bug that had already caused a failure in one of my running projects. I built a multi-ag

Half the Price, Missed the Production Bug: Opus vs Sonnet as Code Reviewers

Sonnet 5.5 reviewed four codebases for half of what Opus 5.5 cost and did not report a single false finding. It also missed the one bug that had already caused a failure in one of my running projects.

I built a multi-agent code-review tool for my own use, a Claude Code plugin that reviews a whole backend at once instead of one PR at a time. It started as a handful of prompts, grew about twenty static-analysis tools, and ended up running them in container images. Before relying on it I wanted to know if the cheaper model would do, and whether the tools were worth it. So I ran both models over four small private projects of mine, with and without the tool images, and had blind judges check every finding against the code.

The problem: nobody reviews the whole service

PR review sees a diff. It never sees the service as a whole: a fork taken over from upstream, a small proxy that works and so nobody reads, a module that grew through a hundred small PRs that were each fine on their own.

The obvious shortcut, "here's my repo, review it", gives you a long list of plausible-sounding notes. Some are wrong, many are style, and you can't tell which ones are worth an afternoon. I wanted three things instead:

  • Coverage. Every file is read by an agent whose job is that module and nothing else, so nothing is skipped because the context filled up.
  • Findings you can trust. Each finding names a file and line, and a separate agent tries to refute it before it reaches the report.
  • A ranking by consequence, not by category. "Will break in use" first, "will cost money or incidents" second, "will rot" last.

Stage 1: just prompts

The first version ran on prompts alone: Claude Code subagents with different roles reading the code, and no static-analysis tools. A subagent is a markdown file: frontmatter with its name and allowed tools, then the prompt. The reviewers get read-only tools only:

---
name: module-reviewer
tools: Read, Grep, Glob, Bash
model: inherit
---

You review ONE module of a backend codebase for defects that matter in
production. You never edit files. Bash is for read-only commands only
(`git log`, `git grep`, `rg`, `wc`, read-only scripts); never install
packages, run the app, run migrations or write outside the run directory.

The role prompts are still the core of the tool. Most of their length goes to rules that kill weak findings. A few excerpts from the current versions:

The module reviewer (correctness, contracts, concurrency, dead code):

Every finding must answer: "If this ships as-is, what concretely goes wrong,
for whom, and how badly?" If you cannot answer that, drop it.
- Never report style, naming, formatting, missing tests, or anything a linter
  catches.
- Prefer fewer, truer findings. At most 25 per module; if more survive, keep
  the most severe.
- Report exactly which files you read fully, skimmed, or did not open. Never
  imply full coverage you did not have.

The security reviewer:

You look for defects an attacker or a curious tenant can actually exploit.
- Every finding needs a concrete attack: who sends what, through which entry
  point, and what they gain.
- Try to disprove each finding first: an upstream guard, middleware, tenant
  filter or validation kills it.
- No generic advice (rate limiting, CSP, MFA) unless the code shows the
  concrete gap.

The performance reviewer has the strictest bar, because "this could be slow" is the easiest note for a model to write:

Report a finding only when you can state all four:
- (a) Why it is hot: the entry point and the file:line call chain that makes
  the code run per request, per message or per item.
- (b) The size driver: what n is and whether something bounds it. If n is
  bounded and small, there is no finding.
- (c) A cost estimate: queries or calls per request, allocations per item,
  big-O, bytes held, time blocked.
- (d) A cheaper alternative that does not hurt readability or correctness.

And the verifier, which sees every finding from the others:

You are the skeptic in a code review. You receive findings produced by another
reviewer and try to REFUTE each one against the actual code.
When in doubt between confirmed and refuted, choose refuted unless you have
direct evidence.

Every finding gets one of three tiers, ordered by consequence, and the verifier may move it up or down. The whole results section below rests on these three labels, so they are worth pinning down now:

Tier What it means A concrete example
WILL_BREAK Wrong results, data loss, a crash, or a security hole in normal use a code path that returns wrong results on valid input
WILL_COST It still works, but burns latency, money, incidents or toil an unbounded query that slows down as the data grows
WILL_ROT A maintainability problem only: dead code, hygiene, confusion a module no caller reaches any more

Stage 2: the prompts needed tools

Prompts alone hit a ceiling, and it's a structural one. Some facts aren't in the code at all: whether a dependency version has a published vulnerability, whether a package on the registry is malicious or was taken over. Other facts are in the code but too expensive to establish by reading: whether a function is reachable from main at all, or whether a secret was committed three directories away from the module under review.

So the scanners and checks got deterministic tools, one set per language, and the prompts changed to treat their output as leads:

Language Dead code Performance and security hints Dependencies
Go staticcheck, deadcode golangci-lint, gosec govulncheck
TypeScript knip, tsc --noUnused* cve-lite
Python vulture Ruff PERF, ASYNC, S osv-scanner
Rust cargo check, cargo-shear Clippy perf osv-scanner
any gitleaks (secrets) osv-scanner, GuardDog (malicious packages)

The prompts say how to use them. The performance reviewer gets "use the linter hints as leads, not findings", the security reviewer gets "never report a hint without the attack it enables", and the verifier still has to refute dead-code candidates by searching for reflective registration and framework wiring.

That's about twenty tools across four ecosystems, each with its own version, installer and runtime. On the host that means a Go toolchain, cargo, uv, Node, osv-scanner and cve-lite, all at versions that behave the same. Anyone running it would have to install and maintain that before the first review, on macOS, Linux or Windows, and keep it out of the way of their own projects.

Stage 3: the tools moved into images

So the tools moved into container images, versioned together with the plugin: one image per language (core, Go, TypeScript, Python, Rust) plus one for polyglot repos, built on Wolfi and published for amd64 and arm64. The host needs only Docker.

The plugin picks the image itself. It looks at which marker files the repository tracks and takes the image for that language, the polyglot image for several, or the core image (secrets, advisories) for none:

const MARKERS = {
  go: /(^|\/)go\.mod$/,
  node: /(^|\/)package\.json$/,
  python: /(^|\/)(pyproject\.toml|requirements[^/]*\.txt|setup\.py)$/,
  rust: /(^|\/)Cargo\.toml$/,
};
const languages = Object.keys(MARKERS).filter((lang) => files.some((f) => MARKERS[lang].test(f)));
const target = languages.length === 1 ? languages[0] : languages.length ? 'all' : 'core';

The image tag matches the plugin version, so a plugin update brings the matching tool versions with it. Without Docker the plugin falls back to whatever tools the host has, and the report says which were skipped.

The images had their own surprises. Trivy in CI found HIGH vulnerabilities in my own tool images, which is an awkward thing for a review tool to ship. I swapped a secret scanner for the one Wolfi packages, built one Go linter from source, removed pip from a Python venv, and dropped a Kubernetes linter whose dependencies I couldn't get clean. The images run as a non-root user and pull fresh offline vulnerability databases on every run. The malicious-package check for Python was the slowest step at 765 seconds on a cold run; caching results per package version brought a warm run down to 3 seconds.

This is the pipeline today:

Review pipeline: collect, per-module reviewers, verifier, report

The experiment

Four small private projects of mine, one per language:

Project Language Reviewed
A Go ~830 lines
B TypeScript ~1,370 lines
C Rust ~1,220 lines
D Python ~11,250 lines (2 modules)

For each project I ran collect once and gave both models the identical inventory, the same agent prompts and the same five aspects (defects, dead code, security, performance, architecture). Only the model changed. Claude Code workflows take a model per agent call, so the switch was one argument:

const modelOpt = model ? { model } : {}
// ...
agent(reviewPrompt(moduleId), { ...modelOpt, agentType: 'module-reviewer', schema })

Then the judging. For each project a judge agent got both finding lists, labelled A and B in a shuffled order, and had to read the code and, for every distinct issue, decide: real, partly true, or false, and which tier is correct. For the Go project I also had a reference: an earlier run where a prover agent had confirmed its main bug with a failing test.

Results: Opus 5.5 vs Sonnet 5.5

Across the four projects the judges found 99 distinct real issues.

Opus 5.5 vs Sonnet 5.5: cost $30.85 vs $15.79; costly issues found 95% vs 81% (35 of 37 vs 30 of 37); WILL_BREAK 2 of 2 vs 1 of 2; WILL_ROT 41 of 60 vs 44 of 60

Neither model made things up. Both lists held up line by line, and both repeated themselves across the module, security and architecture passes. On dead code and maintainability Sonnet was as good as Opus, slightly better even. It was also faster: 16.1 minutes against 23.9 for the four runs.

54 of the 99 issues were found by both models. The difference is in the ones only one model caught:

Who found each real issue. WILL_BREAK: 1 by both, 1 only by Opus. WILL_COST: 28 by both, 7 only by Opus, 2 only by Sonnet. WILL_ROT: 25 by both, 16 only by Opus, 19 only by Sonnet

The gap is at the top of the list.

The bug that had already bitten. In the Python project, a WILL_BREAK defect that the git history showed had already caused a failure once the project was running, where the earlier fix had closed only one of the affected paths. Opus found it in both of its runs. Sonnet missed it in both.

The proven bug, rated too low. In the Go project, a WILL_BREAK defect that an earlier run had already confirmed with a failing test. Sonnet found it, but rated it WILL_COST, so in its report it would sit in the middle of the list instead of at the top.

The most serious issue in the Rust project: Opus found it in both of its runs, Sonnet in one of two.

The tiering pattern matters more than the counts. Opus errs high: the judges moved 8 of its tiers down and none up, among them three issues in the TypeScript project that it called WILL_BREAK and the judge rated WILL_COST. Sonnet errs in both directions: 7 tiers too high and 5 too low. A report is read top-down. An inflated tier wastes a few minutes of a reader's time. An under-rated one hides the thing you ran the review for.

Results: were the images worth it?

To check stage 3, I ran all four projects again without Docker, on whatever tools my laptop had, with both models. This is what the images changed:

What the tool images changed, with images vs host tools. Opus: cost -16.7% ($37.03 to $30.85), tokens -25.6% (64.8M to 48.2M), run time -15.2% (28.2 to 23.9 min), findings kept -17.6% (108 to 89). Sonnet: cost -0.8% ($15.91 to $15.79), tokens -4.2% (35.6M to 34.1M), run time -19.9% (20.1 to 16.1 min), findings kept +25.7% (70 to 88)

I expected the extra tool output to cost tokens. It did the opposite. With the linter and scanner results in front of it, Opus used 26% fewer tokens and cost 17% less, because it didn't have to find the same things by reading. Sonnet cost the same either way, but kept 26% more findings with the images (88 vs 70). Both finished faster. In money: Opus went from $37.03 to $30.85 across the four projects, Sonnet from $15.91 to $15.79.

Some findings exist only because a tool produced them. A known dependency vulnerability and an unused dependency came from the scanners, so no host-tool run could report them. Even then, only one of the two image runs passed them on: Opus saw the same scanner output and left both out.

Limits

  • One run per cell. The variance is visible: Opus without images missed a high-severity bug that every other run found, and Sonnet found the most serious issue in the Rust project in one run out of two. Treat single-digit differences as noise.
  • The judges were Opus. They were blind to which list came from which model, but they are the same model family as one contestant.
  • Small projects. Four of them, about 15,000 reviewed lines in total. A large monolith may behave differently.
  • My laptop is not a clean machine. It has the Go and Node toolchains installed, so "host tools" for the Go project still had most of its linters. On a clean machine the gap between the two image columns would be larger. I only judged the runs with the images; the host-tool runs are compared on cost, time and finding counts.
  • Harness mistakes. My first batch of runs failed because the session didn't know a newly added agent type, and one failed agent took down its whole module with it. I fixed that so each reviewer fails alone. I also passed a wrong module id for the TypeScript project in all four of its runs; the agents reviewed the right directory anyway, and the mistake is the same in every run.

What I'm doing with it

My take: Opus stays the default for any review where missing a breaking bug is the expensive outcome, which is most of them. Sonnet is now a one-flag option for frequent passes and dead-code sweeps, where it matches Opus at half the price. The tool images stay on, because they make the expensive model cheaper and the cheap model better, and nobody has to set up twenty tools by hand.

What I haven't tested yet is a mixed setup: Sonnet for the module and performance reviewers, Opus for security and the verifier. That's the next run.

The cheaper model finds almost everything. Whether that's enough depends on how expensive the "almost" is for you. Would you trust a review that is 95% right at the top of the list, or 81%?

Appendix: what's in the images

Every tool below is free and open source. Versions are from the images as built on 2026-11-08. The base is Wolfi; tools that Wolfi packages come from its signed packages, the rest are pinned by version and checksum or built from source.

Shared by every image (the core image is just this):

Tool Version Used for
osv-scanner 2.7.0 dependency advisories, offline database
GuardDog 3.2.0 malicious-package and takeover signals
gitleaks 8.30.1 secrets committed in source files
actionlint 1.7.12 CI workflows (opt-in infra aspect)
hadolint 2.15.1 Dockerfiles (opt-in infra aspect)
ShellCheck 0.11.0 shell scripts (opt-in infra aspect)
Node.js 26.13.0 runtime for the collector
Python 3.13.18 runtime for GuardDog
git 2.56.0 file lists and history

Added per language:

Image Tool Version Used for
Go Go 1.27.3 toolchain, go list import graph
Go golangci-lint 2.15.0 performance linters and gosec
Go govulncheck 1.9.0 advisories with call-graph reachability
Go staticcheck 2026.2.2 unused code (built from source)
Go deadcode x/tools v0.52.0 functions unreachable from main
TypeScript knip 5.88.1 unused files, exports and dependencies
TypeScript madge 8.0.0 import graph and cycles
TypeScript cve-lite 1.38.0 advisories for npm, pnpm, yarn and bun lockfiles
Python Ruff 0.17.4 PERF, ASYNC and S rules
Python vulture 2.16 unused code
Python uv 0.13.7 dependency export for GuardDog
Rust Rust stable 1.99.1 cargo check lints and Clippy perf
Rust rustup 1.29.2 toolchain management
Rust cargo-shear 1.14.0 unused dependencies

TypeScript's tsc --noUnused* check uses the compiler from the project's own node_modules, so it isn't in the image.

Tool image sizes: Rust 2.32 GB, Go 1.26 GB, TypeScript 1.09 GB, Python 940 MB, core 842 MB

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.