CCLearn

Verifying work that has no test — screenshots, runs, logs

verified against Claude Code 2.1.261 · 2026-09-0916 min

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 /verify yourself 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.

SkillPurpose
/runLaunch and drive your app to see a change working
/verifyBuild and run your app to confirm a code change does what it should, without falling back to tests or type checks
/run-skill-generatorTeach /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:

/run and /verify work without setup. They infer the launch from your project type (CLI, server, TUI, browser-driven) and from what's in your README, package.json, or Makefile. — 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):

ResultWhat Claude gets
ValidInline 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
FailureInline 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.

bash
# 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.log

Widening 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 changeThe checkThe evidence you should see
Visual or layoutScreenshot, compared to the referenceA list of differences, ideally empty
A CLI or a script/verify, or the command run directlyWhat it printed, and its exit code
A service behaviourThe app launched and drivenThe observed response, not a summary of it
A long build or jobMonitor, or a background commandThe failure line, or an explicit "no matches"
Anything at all, second pass/verify yourself, after Claude's checkYour 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

  1. 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 answer

    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 /verify yourself afterwards, because running the app is a documented verification step and the absence of a regression suite changes nothing about that.

  2. 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 answer

    Run /run-skill-generator once on that project. /run and /verify work with no setup by inferring the launch from the project type and from the README, package.json or Makefile, 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, /verify and 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 /verify prompt 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 /verify also records a recipe of its own at .claude/skills/verify/SKILL.md when 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.

  3. 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 answer

    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, a tee, or a runner that prints ERROR and 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, [, plus git diff and git grep — so a check built on pgrep, jq -e or cmp gets 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 with grep -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. Raising bashOutputMaxChars helps only if your builds sit routinely just over the line — it is a bigger bucket, not a verdict — and BASH_MAX_OUTPUT_LENGTH does not move the inline ceilings at all.

Sources