All lessons › Function C — Application Security with an AI SDLC › The AI SDLC — an Agentic AppSec Pipeline, Before and After Deploy
C2.1

What building a harness means in security engineering

C2
▶
Lightboard recording

Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.

Video placeholderpublishes automatically from recordings/C2.1.mp4
Risk

A 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.

Control

An independent verifier, and a budget that stops the loop when it cannot pass.

The lab

Run the same loop twice — once with no verifier, once with one.

◉Use case relevance

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.

At CyberTravels. 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 this lesson is — Day 0, Day 1, Day 2

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.

Day 0 — why

A harness whose verifier is the model agreeing with itself does not fail loudly. It succeeds quietly and wrongly.

Day 1 — how

Build an independent verifier, and a budget that stops the loop when it cannot pass.

Day 2 — measure

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 measuredenominator or target
agreement between the verifier and held-out ground truthrate
loops stopped by budget rather than by successcount

▦The framework, and how it works

   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 CI security scan. Sees the changed files, may only read, "worked" means a zero exit and a finding that parses, stops on a timeout.
  • An autofix bot. Sees the finding and the file, may open a branch, and "worked" should mean the exploit no longer reproduces — not the scanner went quiet.
  • A SOC triage assistant. Sees the alert and its enrichment, may query the SIEM read-only, and "worked" means its disposition matched an analyst's on a held-out sample.

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.

2 · The four moves

movewho does itthe security decision in it
planthe modelnone — this is the part you did not write
actthe harnessthe tool surface: what is even expressible
verifysomething independentwhat the pipeline is allowed to believe
stopthe harnessthe budget — the last control still standing
Frameworks make plan and act easy and leave verify and stop as your problem, usually defaulting to “the model says it is done” and “loop forever”.

3 · One loop, run twice

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.

4 · The part worth keeping

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?

▶Real time execution as skill

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, Glob

The loop is yours; the model is a component in it

A 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.

When to use this

Whenever you are about to put a model in a for-loop: triage, remediation, detection authoring, anything that iterates until it is satisfied.

Procedure

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.

Example

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.

Output contract

{
  "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.

Failure modes

  • Offering the model a DONE exit. It will take it on the first turn.
  • A verifier that accepts one spelling. It rejects correct work, repeatedly, and the budget goes on retries.
  • Asking a model to judge the answer. That is a second opinion, not a verification.

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.

1In your AI agent — you are taught

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

Then 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:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons C2.1type /c2-1-what-building-a-harness-means-in
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons C2.1type /c2-1-what-building-a-harness-means-in in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons C2.1type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons C2.1type $c2-1-what-building-a-harness-means-in, or run /skills

2As one Python command — you just run it

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

3Read the code, and the difference — nothing runs

Neither 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 --diff

On 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.

✓What you just proved

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.

✎Your turn

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

← PreviousC2.0 · The AI SDLC — what runs before a deploy, and what runs after