Verifying work that has no test — screenshots, runs, logs
By the end you can
- Construct a pass/fail check for UI, service and script work with no suite
- Use /verify and /run to confirm a change against the running app
- Record a launch recipe once instead of re-inferring it every session
- Ask for a screenshot comparison that returns a difference list, not an opinion
- Watch a long-running check with Monitor or a background command
- Recognise evidence that was truncated before Claude ever read it
The objection arrives on the next task after Lesson 5.1 convinces you. It is a padding change on a dashboard. It is a startup script. It is one endpoint in a service whose suite covers everything except this. There is no test to hand over, so the loop stays open and you go back to reading diffs by eye.
Untested is not unverifiable
Re-read the definition from Lesson 5.1 and notice what it does not say. It does not say "a test suite". It says a signal Claude can read in the conversation, and a test suite is the first item on a list of five. The sentence immediately after it is this lesson's whole thesis:
Run
/verifyyourself after Claude's check passes to confirm the change against the running app. — Best practices
Running the app is a documented verification step, recommended after Claude's own check has passed — whatever that check was. So "there is no suite" was never the same statement as "there is no signal". A signal only has to come back different when the work is wrong, and a screenshot, a launched app and a tailed log all do.
Build it and drive it: /run and /verify
Three bundled skills exist for exactly this case, typed like any other slash command.
| Skill | Purpose |
|---|---|
/run | Launch and drive your app to see a change working |
/verify | Build and run your app to confirm a code change does what it should, without falling back to tests or type checks |
/run-skill-generator | Teach /run and /verify how to build and launch your project |
Read the middle row's last clause again. /verify is not a fallback for projects
too immature to have tests; its documented purpose is confirming a change
without leaning on tests or type checks, because a green suite and a working
app are different claims. /run when you want to look at it, /verify when you
want a verdict.
Recording the recipe once
The zero-setup version has a documented ceiling:
/runand/verifywork without setup. They infer the launch from your project type (CLI, server, TUI, browser-driven) and from what's in your README,package.json, orMakefile. — Skills
Inference is fine for a standard launch and gets unreliable for anything that
needs a database, an env file, a graphical session, or a multi-step build — the
projects where you most wanted the check. The fix is not a better prompt each
time: "/run-skill-generator records the recipe instead." It gets the app
running from a clean environment, captures what worked — the install commands,
the env vars, the launch script — and commits it as a per-project skill at
.claude/skills/run-<name>/. After that, /run, /verify and any other agent
in the repo follow the recorded recipe instead of rediscovering it. Run it once
per project, and again when the build or launch process changes.
That is the same on-disk-versus-in-the-moment rule that decided what survives
compaction in Lesson 3.2. A launch procedure you explain in chat is gone at the
next /clear; one in .claude/skills/ is there for every future session and
every teammate.
Screenshots as a pass/fail
The middle row of Lesson 5.1's rewrite table is the one this lesson is built on: "make the dashboard look better" becomes a pasted screenshot plus an instruction to compare the result against it and fix the differences. Read it as two separate moves.
Move one is the reference image, pasted rather than described. Lesson 2.2 has the three documented ways to get one in; each is a keystroke, and none of them is the interesting half.
Move two is what turns an image into a check. Compare, list differences, fix. Not "does this look right" — that returns an opinion, and an opinion reads the same whether the work is right or wrong. A difference list is empty or it is not, and an empty one is a pass. Name what must match — spacing, the states you care about, the breakpoint — and the comparison has something to be wrong about. Ask for the comparison, not the conclusion.
Watching a log instead of polling it
Some checks do not finish while you are looking at them: a long build, a migration, a CI job, a dev server that only misbehaves under traffic.
The Monitor tool lets Claude watch something in the background and react when it changes, without pausing the conversation. — Tools reference
The documented uses are almost a list of the checks this lesson is about: tail a log and flag errors as they appear, poll a PR or CI job and report when its status changes, watch a directory, track output from a long-running script. For most watches Claude writes a small script, runs it in the background and receives each output line as it arrives; you keep working and Claude interjects.
Monitor runs its commands under the same permission rules as Bash, so
allow and deny patterns you already set apply here too. It is unavailable on
Amazon Bedrock, Google Cloud's Agent Platform and Microsoft Foundry, and when
DISABLE_TELEMETRY or CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is set —
check before designing a workflow around it. The plainer sibling is a background
command: run_in_background: true for a dev server or watch build, with
/tasks to list and stop what is running.
The evidence nobody actually read
Here is the failure that makes this whole lesson collapse if you miss it. A command's output does not necessarily reach Claude. What arrives depends on whether Claude Code treats the result as valid or as a failure (Tools reference):
| Result | What Claude gets |
|---|---|
| Valid | Inline up to roughly 30,000 characters by default; past that, a saved file path plus a short preview from the start, and Claude reads or searches the file when it needs the rest |
| Failure | Inline up to roughly 10,000 characters; past that, a head-and-tail excerpt of that size, with no file path |
Read the asymmetry. The failing case — the one you actually needed to see — has the smaller inline budget, though its excerpt at least keeps a tail. The valid case is the treacherous one: past the ceiling the preview comes from the start of the output, which for a verbose build or a full-suite log is the banner, not the verdict two thousand lines down.
A second edge has the same effect. Exit code 1 counts as a valid result only for
a recognised short list — grep, rg, egrep, fgrep, find, diff,
test, [, plus git diff and git grep. Every other command exiting 1
counts as a failure even when 1 is benign for it: no matches for pgrep and
jq -e, files that differ for cmp. Build your check on pgrep, jq -e or
cmp and a perfectly fine run gets the failure treatment, and the smaller
budget with it.
So: a check whose output overflows is not a check until something reduces it to a verdict.
# Assert the exit code; show the tail only when it fails
npm run build > /tmp/build.log 2>&1 && echo BUILD_OK || tail -40 /tmp/build.log
# Or reduce the log to a countable marker
grep -c -E '^(FAIL|ERROR)' /tmp/build.logWidening the window helps only if you widen the right one.
BASH_MAX_OUTPUT_LENGTH sets how many characters Claude Code reads back from the
working file — 30,000 by default, hard ceiling 150,000 — and raising it does
enlarge the window a failing command's head-and-tail excerpt is cut from. But it
does not raise the inline ceilings, so a large valid result still arrives as a
file path plus preview. The setting that moves the valid-result ceiling is
bashOutputMaxChars, up to 128,000 characters: it sizes that inline ceiling and
the read-back window together, and Claude Code then ignores
BASH_MAX_OUTPUT_LENGTH altogether. It requires v2.1.261 or later. Raise
bashOutputMaxChars if your builds sit routinely just over the line — but it is
a bigger bucket, not a verdict, so reduce the output as well.
Choosing a check
| Your change | The check | The evidence you should see |
|---|---|---|
| Visual or layout | Screenshot, compared to the reference | A list of differences, ideally empty |
| A CLI or a script | /verify, or the command run directly | What it printed, and its exit code |
| A service behaviour | The app launched and driven | The observed response, not a summary of it |
| A long build or job | Monitor, or a background command | The failure line, or an explicit "no matches" |
| Anything at all, second pass | /verify yourself, after Claude's check | Your own eyes on the running app |
None of this is a lower standard than a test suite. It is the same standard, reached by a different route, and the rule underneath it does not move: if you cannot say what would have made this fail, you have not verified anything — you have read a summary. Lesson 5.2 gave you the places to site a check; this one built one where you thought none existed. Lesson 5.5 is the last problem, when the check runs, comes back green, and is still wrong.
Checkpoint
-
You changed the padding on a settings panel. There is no visual regression suite and there never will be. Write the request that makes this change verifiable, and say which part of it is the check.
Show answerAnswer
Paste the reference image and ask for a difference list. The request is 5.1's middle rewrite applied to your panel: "[paste screenshot] this is the spacing I want on the settings panel. implement it, take a screenshot of the result, compare it to the original, list the differences and fix them." Name what must match — the spacing, the states you care about, the breakpoint — so the comparison has something to be wrong about.
The check is the difference list, not the screenshot. The image is move one; move two is what turns it into a check. "Does this look right" returns an opinion, and an opinion reads the same whether the work is right or wrong, while a difference list is empty or it is not and an empty one is a pass. Ask for the comparison, not the conclusion — then run
/verifyyourself afterwards, because running the app is a documented verification step and the absence of a regression suite changes nothing about that. -
A teammate reports that
/verify"didn't work" on their project — it built the wrong target and gave up. What should they do so the next person does not hit the same wall?Show answerAnswer
Run
/run-skill-generatoronce on that project./runand/verifywork with no setup by inferring the launch from the project type and from the README,package.jsonorMakefile, and that inference is exactly what gets unreliable when a project needs a database, an env file, a graphical session or a multi-step build. The generator gets the app running from a clean environment, captures what worked — the install commands, the env vars, the launch script — and commits it as a per-project skill at.claude/skills/run-<name>/, after which/run,/verifyand any other agent in the repo follow the recipe instead of rediscovering it. Re-run it when the build or launch process changes.The tempting wrong fix is to explain the right target to Claude in chat, or to write a better
/verifyprompt each time. That is the same on-disk-versus-in-the-moment rule as Lesson 3.2: an explanation in chat is gone at the next/clear, while.claude/skills/is there for every future session and every teammate. Worth knowing that/verifyalso records a recipe of its own at.claude/skills/verify/SKILL.mdwhen it has to work the launch out with nothing on disk to follow — but here it gave up, so nothing was recorded. Record it on purpose. -
Claude ran the full build, said the log was clean, and moved on. What are the two mechanisms that could have prevented it from ever seeing the failure, and what would you have asked for instead?
Show answerAnswer
Both mechanisms are about how the output reached Claude, not about the build. First, the preview comes from the start: a valid result past roughly 30,000 characters arrives as a saved file path plus a short preview taken from the beginning of the output, and the beginning of a run that errors two thousand lines down is identical to the beginning of one that does not. Second, the exit code was not the verdict — a build behind
|| true, atee, or a runner that printsERRORand still exits 0 is treated as a valid result, which is the treacherous branch. (A related edge to keep in mind: exit 1 counts as valid only for a recognised short list —grep,rg,egrep,fgrep,find,diff,test,[, plusgit diffandgit grep— so a check built onpgrep,jq -eorcmpgets the failure treatment and its smaller inline budget.)Ask for a verdict rather than a log:
npm run build > /tmp/build.log 2>&1 && echo BUILD_OK || tail -40 /tmp/build.log, or reduce the log to a countable marker withgrep -c -E '^(FAIL|ERROR)' /tmp/build.log. And when someone says the log looked clean, ask which command produced it and what it exited with. RaisingbashOutputMaxCharshelps only if your builds sit routinely just over the line — it is a bigger bucket, not a verdict — andBASH_MAX_OUTPUT_LENGTHdoes not move the inline ceilings at all.