Skip to content

Agent review

cappa agent turns a failed visual run into something a coding agent can review cheaply. A heuristic labels every changed screenshot from its diff interpretation; only the changes it cannot settle are handed to the agent, as small crops of each changed region instead of full-page screenshots. The agent writes a verdict, Cappa folds it back in, and the agent approves what you confirm — or, because it has your code, fixes what is broken.

The approach follows BlazeDiff’s agent review, adapted to Cappa’s plugins and baselines.

Install the skill into your project from the directory holding cappa.config.ts:

Terminal window
cappa agent init

The files always land in the project root — the nearest directory above with a lockfile or pnpm-workspace.yaml — because that is where agents look for skills. So in a monorepo where only one package depends on Cappa, run it in that package (pnpm -F web exec cappa agent init), and the skill still goes to the root.

What gets installed depends on the agents the project already uses. You can also pick with --stack:

Stack Detected by Installs
claude .claude/, CLAUDE.md (also the fallback) the skill in .claude/skills/cappa/, plus a cappa-judge subagent in .claude/agents/ that judges screenshots in its own context (below)
agents .agents/, AGENTS.md the skill in .agents/skills/cappa/ (the vendor-neutral Agent Skills directory, read by Codex and others)
opencode .opencode/, opencode.json(c) the .agents skill, plus OpenCode commands and a judge subagent (below)

--stack all installs everything. The skill is SKILL.md, JUDGING.md and MASKING.md.

The installed files call Cappa the way your project runs it: pnpm exec cappa, yarn cappa, bunx cappa or npx cappa, picked from the lockfile. When Cappa lives in a workspace package, the command switches into it — pnpm -C packages/web exec cappa, yarn workspace web cappa, bun --cwd packages/web x cappa or npm exec -w packages/web -- cappa — so agents run it from the root. Override it with --runner "<command>". Commit the files and tailor them to your project. Re-running init skips files you modified, unless you pass --force.

When cappa.config.ts has no webServer, init points that out: without one, the agent has to work out how to start your Storybook or app by itself. init never edits the config.

Then ask your agent for a visual review, or run /cappa in Claude Code.

OpenCode reads the skill from .agents/skills/. The opencode stack also adds commands that run the review flow, and a subagent that keeps the judging out of your main conversation:

File What it does
.opencode/commands/cappa-review.md /cappa-review [request] injects the current agent check state. It captures when asked or when nothing is captured yet, hands pending judgments to cappa-judge, reports one line per screenshot and offers next steps.
.opencode/commands/cappa-judge.md /cappa-judge [names] runs the judge as a subtask on the pending screenshots, or on the ones named.
.opencode/commands/cappa-approve.md /cappa-approve <names or label> approves by exact name or verdict label. It holds back regressions and deletions unless you name them explicitly.
.opencode/agents/cappa-judge.md A cappa-judge subagent. It reads the region crops and the git history in its own context, and records verdicts with cappa agent judge.

The judge works in a sandbox. It cannot edit files, fetch from the web or start other agents. Its shell is not narrowed to particular commands: OpenCode’s permission patterns match command text literally, so a list of allowed commands breaks as soon as the runner, the shell or the quoting differs. Its instructions limit it to cappa agent check, cappa agent judge and read-only git (diff, log, show, status), so it records verdicts but does not approve, capture or change code.

The commands and the agent use the runner chosen at install time, because OpenCode runs their shell steps before the model sees them.

Judging means reading images. A subagent runs on the model that started it, so if your main agent uses a model that cannot read images, the judge cannot either. Pin the judge to one that can with --judge-model:

Terminal window
cappa agent init --judge-model anthropic/claude-sonnet-5-5 # OpenCode: provider/model
cappa agent init --judge-model sonnet # Claude Code: alias or model id

The model goes into the model: field of the judge whose format it matches. init refuses a model that no installed judge can take. Changing the model rewrites the judge without --force. Re-running init without the flag keeps the pinned model. Without a pin, init warns about the OpenCode judge. Every Claude model can read images, so the Claude Code judge works without one.

A judge that still finds it cannot see the crops records nothing. It replies with the --judge-model command to run instead of guessing from the region metadata. Don’t rely on that check alone: a model that cannot read images does not always notice. Pinning a vision model is what makes judging work.

Claude Code’s agent files cannot limit Bash to particular commands, so the Claude Code cappa-judge uses a PreToolUse hook instead. Before every shell command the judge runs, the hook calls cappa agent guard, which allows only cappa agent check, cappa agent judge and read-only git. It refuses anything else, including chaining, pipes, redirects and command substitution.

Each judgment comes with two images, so a single judge cannot take hundreds of them. The skill gives each judge about 20 screenshots and runs several judges in parallel. Screenshots that share a headline stay with the same judge.

If the project also has .claude/skills/cappa, OpenCode uses that copy of the skill instead of the one in .agents/skills, and init points the OpenCode judge at it too. Tailor the .claude copy, and keep .agents/skills in sync for other agents.

  1. Capture: the agent runs cappa capture --json (see machine-readable output). With a webServer, that also starts and stops the app under test.

  2. Check — cappa agent check --json labels every changed screenshot:

    Label Meaning Next step
    regression-likely Structural change (content added, removed or replaced) at medium or high severity Check against the code, then fix
    intentional-likely Confident recolour, or content shifted/restyled with structure intact Approve if intended
    noise-likely Rendering noise, or a sub-perceptual (< 0.01 %) low-severity change Mask or approve
    ambiguous Anything else, including changed image dimensions Judged by the agent
  3. Judge — for each ambiguous change, Cappa writes a judgment request the agent reads instead of the full screenshots:

    • locator.png — a thumbnail with every changed region outlined and numbered
    • regions.png — one expected | actual row per region, cropped with context
    • overview.png — expected and actual side by side, when the dimensions changed
    • request.json — regions, heuristic label, interpretation summary and paths

    The agent records its verdict with cappa agent judge and re-runs cappa agent check, which applies it. To get the same evidence for changes the heuristic already labelled, the skill runs cappa agent check --judge all. The skill does this when a heuristic label looks doubtful, for example a regression-likely that a deliberate code change explains. A request stays pending until it gets a verdict, so after --judge all the agent judges every change. When the cappa-judge subagent is installed, the agent always hands the judging to it, so a judge pinned with --judge-model is never bypassed. Without one, an agent whose model cannot read images hands the judgment directories to any subagent that can, rather than working from the raw screenshots.

  4. Act — the agent reports one line per screenshot, asks you to confirm, and approves with cappa agent approve. A regression can be fixed in code and re-captured with --filter.

cappa agent check never captures: it reads the screenshots the last cappa capture left on disk, so it is safe to run as often as needed. It works without diff.interpret — when a capture did not persist an interpretation, it is computed on demand.

cappa review shows the verdicts too. Changed screenshots get a verdict badge in the list and grid views. A robot icon marks a verdict an agent wrote; a magnifier marks the heuristic’s label. The detail page shows the headline, the reasons, the confidence and when the check ran. You can look at what the agent concluded, agree or disagree, and approve from the same screen.

A verdict is shown only while it still describes the images on disk. Once a screenshot is re-captured or approved after the check, its verdict disappears until the next cappa agent check. A verdict written with cappa agent judge shows up after the next check applies it.

Terminal window
cappa agent check --json

Prints { reportPath, total, passed, changed, new, deleted, pendingJudgments, results }, where results holds one entry per non-passing screenshot:

{
"name": "example/button/primary",
"category": "changed",
"verdict": {
"label": "ambiguous",
"headline": "2 regions: 1 content-change, 1 shift @ center (1.20%, medium)",
"action": "investigate",
"source": "heuristic"
},
"pending": true,
"judgment": "/abs/screenshots/.cappa-agent/judgments/example/button/primary"
}

Headlines are built so the same kind of change produces the same text on every screenshot, which lets an agent spot one shared cause — a footer date, a theme toggle — without opening any image. The full report, with regions and absolute paths, is written to reportPath.

Option Description
--judge <mode> ambiguous (default) requests judgments for ambiguous changes only; all for every changed screenshot; none for none. Existing judgments are always kept and applied.
--max-tile-regions <n> Regions rendered into each regions.png (default 8).
--json Print the summary as JSON and nothing else.

Exits with 1 while any screenshot is changed, new or deleted, like cappa status.

Terminal window
cappa agent judge example/button/primary --label regression-likely \
--headline "Submit label truncated" --rationale "'Submit' renders as 'Subm'" --confidence 0.9 --json

Records a validated verdict for one or more screenshots with a judgment request. --label is regression-likely, intentional-likely or noise-likely. Passing several names records one shared verdict.

A verdict is tied to a fingerprint of the expected and actual images it was made about. When a re-capture changes either image, the verdict is dropped and the screenshot is judged again.

Terminal window
cappa agent approve example/button/primary --json
cappa agent approve --label intentional-likely --json

Approves screenshots by exact name — unlike cappa approve --filter, which matches substrings — or every changed screenshot whose current verdict carries a label. Refused when CI is set, so baselines only change on developer machines.

It also refuses any screenshot whose current verdict is regression-likely. If the change was intended, judge it intentional-likely first. To approve a regression anyway, pass --allow-regression.

Installs the skill files described in Setup. --stack, --runner and --force are covered there; --judge-model <model> pins the judge subagent to a model that can read images (see The judge’s model).

Everything lives in <outputDir>/.cappa-agent/, which carries its own .gitignore — judgments are only valid for the screenshots on the machine that made them.

.cappa-agent/
├── report.json
└── judgments/<screenshot name>/
├── request.json
├── locator.png
├── regions.png # or overview.png
└── verdict.json # written by `cappa agent judge`