Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/C2.1.mp4A harness whose verifier is the model agreeing with itself does not fail loudly. It succeeds incorrectly, files a clean trace, and the bug is found by whoever merged the patch.
An independent verifier, and a budget that stops the loop when it cannot pass.
Run the same loop twice — once with no verifier, once with one.
A research function that produces papers is a cost centre with good intentions. One that produces controls somebody else deploys is a capability, and the difference is decided at scoping, not at publication.
appsec/findings.py. A Finding carries its basis — pattern, model, reachability, executed exploit — so stage 7's hypothesis cannot be reported with stage 12's confidence. run() refuses when the producer and the verifier are the same callable, and refuses a verifier that cannot answer undetermined. Your smoke test proves both refusals, because a self-grading loop files a clean trace.What separates a harness from a loop with a model in it: structured output, a verifier that is not the producer, and a budget that stops with the work unfinished. Built around the failure that does not announce itself — a model grading its own findings files a clean trace either way.
A harness whose verifier is the model agreeing with itself does not fail loudly. It succeeds quietly and wrongly.
Build an independent verifier, and a budget that stops the loop when it cannot pass.
Agreement rate between the verifier and held-out ground truth, and the number of loops stopped by budget rather than by success.
Measure this
| what to measure | denominator or target |
|---|---|
| agreement between the verifier and held-out ground truth | rate |
| loops stopped by budget rather than by success | count |
research output what makes it a capability +----------------+ +--------------------------+ | a paper | | a control someone deploys| | a talk | vs | an eval case in CI | | a thread | | a detection that fires | +----------------+ +--------------------------+ decided at scoping: "who will deploy the answer" is the first question
Building a harness, in security engineering, is building everything around a model that turns generating text into getting work done: what it sees, what it may do, how you know whether it worked, and when it stops. A model on its own is a text generator — no memory, no actions, no notion of success. The wrapper is what makes it a control, and every part of that wrapper is a security decision: the tool surface is an authorisation problem, the verifier decides what your pipeline is allowed to believe, and the budget is the only thing still holding once the model is the component you cannot trust.
You already run several, whether or not anyone calls them that:
The third field is where harnesses fail, and they fail quietly. A harness whose verifier is the model agreeing with itself does not stop and report an error — it succeeds incorrectly, files a clean trace, and the bug is found later by whoever merged the patch.
| move | who does it | the security decision in it |
|---|---|---|
| plan | the model | none — this is the part you did not write |
| act | the harness | the tool surface: what is even expressible |
| verify | something independent | what the pipeline is allowed to believe |
| stop | the harness | the budget — the last control still standing |
The simplest harness that shows the point: about twenty lines, one real model call, one SQL injection from CyberTravels' booking service. It runs with no verifier and then with one — same model, same prompt.
Offline the model is a labelled replay; against a served open-weight endpoint it is the identical code.
Read the last block of that output again. The answer it rejects — ref=" + escape(ref) — is the one that matters: it reads like a fix, it would pass a human skim, and it is still concatenation. Without a verifier the loop accepts it, reports success and files a clean trace.
That is the whole reason this chapter defines the word before it builds anything. Every stage after this one is a harness, and for each of them the question is the same: what, other than the model, decided that this worked?
The skill — skills/appsec/agentic-harness-loop/SKILL.md
name: agentic-harness-loop
description: >-
Build and check the plan-act-verify loop that turns a model into a harness —
including the exit condition, the parser, and an independent verifier that
decides what the loop is allowed to accept. Use when writing an agentic
pipeline, or when a loop reports success on work nobody checked.
allowed-tools: Read, Grep, GlobA harness is four moves — plan, act, verify, stop — and the security engineer owns three of them. What the loop accepts is decided by the verifier, not by the model, and a loop with no verifier accepts whatever came back and reports success.
Whenever you are about to put a model in a for-loop: triage, remediation, detection authoring, anything that iterates until it is satisfied.
1 — Write the exit condition before the prompt. A loop whose exit is easier than the work will take the exit. Do not offer the model a way to declare itself done; decide that from the outside, on the artefact.
2 — Parse defensively, because parsing is the harness's job. Asked for one line, a small model returns the whole function and a larger one returns a fenced block. Both are reasonable readings. Take what you need from what arrives rather than requiring the model to do you a favour.
3 — Write the verifier as an independent check. Independent means it does not ask the model whether the answer is good. It examines the artefact: does the line use a placeholder, does the test pass, does the exploit stop working.
4 — Test the verifier against a correct answer you did not expect. This is the step that gets skipped. A verifier that accepts only one spelling of correct rejects real work and burns the budget doing it — check %s, :name and $1 before shipping a check for ?.
5 — Test it against a plausible wrong answer. Something that reads like a fix and is not — an escape function wrapped around concatenation. If the verifier accepts it, the loop ships it, reports success, and the trace looks clean.
Input — the fixture committed at the top of scripts/agentic_harness_loop.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.
Output — the opening lines of a real run:
the loop, wired: ['plan', 'act', 'verify', 'stop']
backend : replay
steps : 1
accepted : return DB.execute("SELECT * FROM bookings WHERE ref=?", (ref,))
verified : None <- nothing checked it
The loop stopped because the model produced something, which is not the
same as producing something correct. Whatever came back was accepted.
The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.
{
"loop": {"moves": ["plan", "act", "verify", "stop"], "max_steps": 0, "exit_owned_by": "harness|model"},
"parse": {"expected": "str", "shapes_seen": ["str"], "strategy": "str"},
"verifier": {"independent": true, "accepts": ["str"], "rejects": ["str"]},
"runs": [{"verifier": "none|present", "steps": 0, "accepted": "str", "verified": null}]
}
Report the unverified run too. The pair is the lesson: the same model and the same prompt, with and without something checking the answer.
Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.
The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.
Install it, once. From the folder you cloned. --all does every agent it finds on your machine:
python3 scripts/install_skills.py --all --lessons C2.1Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick c2-1-what-building-a-harness-means-in:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons C2.1 | type /c2-1-what-building-a-harness-means-in |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons C2.1 | type /c2-1-what-building-a-harness-means-in in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons C2.1 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons C2.1 | type $c2-1-what-building-a-harness-means-in, or run /skills |
No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.
python3 scripts/lesson.py C2.1Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.
python3 scripts/checkpoint.py --at C2.1 --out work/cybertravels
python3 scripts/checkpoint.py --at C2.1 --diffOn Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.
Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.
Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.
If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.
The loop runs with a real model behind ask() — a labelled replay offline, a real open-weight call when one is served. With no verifier it accepts whatever came back and reports verified: None. With the verifier the same model and prompt produce an accepted, parameterised line; a narrow verifier that only accepts ? is shown rejecting a correct psycopg fix, and a plausible answer wrapping the input in escape() is refused, because it is still concatenation.
Name your pipeline's verifier out loud. If the sentence contains "the model checks" or "it looks right", you have a judge, and a judge approves confident prose — including prose that contradicts the finding it is attached to.
Skill source: skills/appsec/agentic-harness-loop