Two sessions, same task, same model. In the first, the agent finishes, reports "implemented and working", and you spend forty minutes discovering it is not. In the second, the agent finishes, and the report arrives with test output attached, a green typecheck, and a diff you already know touches only the two files you scoped. The difference is not the agent. It is that the second session had a gate: a machine-runnable definition of done that the loop had to satisfy before "done" was a word it was allowed to use. The docs say this plainly: Claude stops when the work looks done, and without a check it can run, "looks done" is the only signal available, so you become the verification loop.
The loop is the unit of work
Stop thinking in tasks and start thinking in loops. A unit of agentic work is: a dispatch that defines the gate, an agent that iterates against it, a gate that runs machine checks, and an exit that only opens on evidence. Fail feeds back into iteration; pass produces a committable change.
This is 1.2's agentic loop with the missing organ installed. The model already iterates: it edits, runs, reads output, edits again. What it iterates toward is whatever signal is available, and that is the design surface you own. Give it nothing and it converges on "looks plausible". Give it a test suite and it converges on passing tests. The loop is an optimizer; the gate is the objective function; writing objective functions is now part of your job, and the whole discipline of this lesson is making the objective mean what you intend.
Gates come in ascending strength, and the ladder matters because each rung catches what the one below cannot:
- Typecheck. Catches hallucinated APIs (1.5, class one) and shape errors at near-zero cost. The floor for any typed project; a loop without it lets invented symbols survive until runtime.
- Lint. Catches the mechanical style and correctness rules you decided once so nobody argues per-diff.
- Unit tests. The first rung that checks behavior. Only as strong as the assertions, which is why the second half of this lesson is about protecting them.
- Integration. Catches what units cannot: pieces disagreeing about a contract, the wrong wire format, the migration nobody ran.
- Manual verification. You, running the thing, reading the diff. The strongest rung and the only one that checks whether the spec itself was right. Never delegable, for the reason 0.1 gave: everything below this rung was satisfied inside the loop, so someone outside the loop has to confirm the loop was pointed at the right target.
The harness gives you an escalation path for where the gate lives, straight from the current docs. In one prompt: tell the agent the check and have it iterate ("run npm test and fix failures until green"). Across a session: set the check as a /goal condition, and a separate evaluator re-checks it after every turn. As a deterministic gate: a Stop hook runs your check as a script and blocks the turn from ending until it passes (with the cap you learned in 2.6: eight consecutive blocks and the harness overrides). As a second opinion: a verification subagent in a fresh context tries to refute the result, so the agent doing the work is not the one grading it. Each step trades setup for attention; the prompt version works today on any task, the hook and subagent versions are what let an unattended run finish correctly without you.
One rule falls out of all this, and it is the position this lesson defends: if you can't verify it, don't dispatch it. Not because unverifiable work is forbidden, but because dispatching it buys you nothing. The value of delegation is walking away, and you can only walk away from a loop that can grade itself and a result you can check on return. If no gate exists for the work ("make the error messages friendlier"), either build the gate first (a golden file of expected messages, a rubric you will apply by hand) or do the work interactively, watching every step, which is not dispatch, it is pairing. The dispatchable surface of your project grows exactly as fast as its verification surface. That is why 3.7 teaches evals: not as testing trivia, but as the thing that expands what you can safely delegate.
TDD with agents: the test is the spec
Test-driven development predates agents, but agents change its economics completely. For a human, writing tests first is discipline with delayed payoff. For an agent loop, the failing test IS the dispatch: an executable specification the optimizer converges on, precise in exactly the way prose cannot be. "Handle edge cases" is a wish; a failing test asserting formatBytes(1023) returns "1023 B" is a wall the loop cannot claim to have climbed until it has.
The sequence that works, and the docs recommend the same shape (write a failing test that reproduces the issue, then fix it; or have one Claude write tests and another write code to pass them):
- Write the tests from the spec, before any implementation. Draft them yourself or have an agent draft them in a separate session from the spec; either way you review every assertion, because the tests are about to become the definition of correct. This review is the highest-leverage few minutes in the cycle.
- Run them. Watch them fail. A test you never saw fail proves nothing; it might pass vacuously. Red first is evidence the suite can detect the absence of the feature.
- Lock the test files. Now the grader physically cannot be edited by the thing being graded. This is the step agents add to classic TDD, and it exists because of 1.5.
- Dispatch the implementation. The dispatch names the test command as its gate. The loop iterates until green, and green now means something, because the definition of green was written by you and is untouchable.
Step 3 deserves its mechanism spelled out. You learned in 1.5 that reward hacking is the loop optimizing the target instead of the intent, and that editing the test is genuinely the shortest path to green. In 2.6 you built the countermeasure class: a PreToolUse hook that says no with exit 2. Point it at the test files:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/lock-tests.sh"
}
]
}
]
}
}
#!/bin/bash
# Test files are the grader. During implementation dispatches, the
# grader is read-only. Exit 2 = block, stderr goes to Claude (2.6).
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Fail CLOSED: a guardrail that cannot parse its input blocks (2.6).
if [ -z "$FILE_PATH" ]; then
echo "lock-tests: could not read file_path from hook input" >&2
exit 2
fi
case "$FILE_PATH" in
*.test.*|*/tests/*)
echo "Blocked: $FILE_PATH is a locked test file. Tests define the spec for this dispatch; implement against them. If a test is wrong, stop and report it instead of editing it." >&2
exit 2
;;
esac
exit 0
Everything in that script is 2.6 doctrine applied: exit 2 to block, stderr as the course-correction message, fail-closed on unparseable input, quoted variables. The stderr message does double duty; it does not just say no, it names the sanctioned alternative (stop and report), which converts a blocked reward hack into the escape-hatch behavior you actually want. When a test really is wrong, and sometimes it is, the fix routes through you: unlock, fix the test deliberately, re-lock, redispatch. The grader changes by human decision or not at all.
Pair the lock with the reading habit from 1.5: test diffs first, every time, even with the hook installed. The hook covers the files you predicted; your eyes cover the ones you did not (a new test file full of vacuous assertions passes the lock cheerfully). Defense in depth, exactly like the instruction-plus-hook pairing in 2.6.
Where it breaks
The gate that lies. A weak test suite is a weak spec, and the loop will implement the weakness faithfully. Assertions that check "no exception thrown", tests that mock the behavior under test, coverage that exercises lines without checking outcomes: all of these produce a green that means nothing, and an agent loop finds the meaningless green faster than a human would, because it is optimizing. Before you trust a suite as a gate, read it the way you would read a contract you are about to sign.
Green-but-wrong. The tests pass and the feature is still wrong, because the tests encode a misunderstanding of the spec. TDD moves the risk, it does not remove it: the correctness of the whole system now concentrates in the test-writing step, which is exactly why you review assertions there with full attention. The manual rung exists because no machine gate checks whether the target itself was right.
Latency and the disabled gate. A gate that takes six minutes per iteration turns a twenty-iteration loop into a two-hour session, and the predictable human response is to disable the gate, which protects nothing (2.6 said the same about slow hooks). Tier your gates: typecheck and the relevant unit tests inside the loop, the full suite and integration at the end as the exit gate. Fast inner loop, strong outer door.
Verification theater. Running the gates and not reading the results, or letting the agent summarize its own gate output. The docs' advice is to have Claude show evidence rather than assert success: the test output, the command it ran and what it returned. Read the evidence, not the adjective in front of it.
Lablab-3-2TDD one function with the tests locked
Goal: Write a failing test suite as the spec, lock it with a hook, dispatch the implementation, and watch the loop converge on tests it cannot touch.
Prereqs: the agentic-practice repo with the 2.6 hooks working, jq installed, Node 18+ (the lab uses the built-in node --test runner, no packages).
- Write the spec as tests. Create
format-bytes.test.mjsyourself (you, not the agent; this round the point is feeling the test-as-spec role):
import test from "node:test";
import assert from "node:assert/strict";
import { formatBytes } from "./format-bytes.mjs";
test("bytes below 1024 stay in B", () => {
assert.equal(formatBytes(0), "0 B");
assert.equal(formatBytes(1023), "1023 B");
});
test("units scale by 1024 with one decimal", () => {
assert.equal(formatBytes(1024), "1.0 KB");
assert.equal(formatBytes(1536), "1.5 KB");
assert.equal(formatBytes(1048576), "1.0 MB");
assert.equal(formatBytes(5368709120), "5.0 GB");
});
test("values round to one decimal", () => {
assert.equal(formatBytes(1075), "1.0 KB");
assert.equal(formatBytes(1126), "1.1 KB");
});
test("negative and non-finite input throw RangeError", () => {
assert.throws(() => formatBytes(-1), RangeError);
assert.throws(() => formatBytes(Infinity), RangeError);
assert.throws(() => formatBytes(NaN), RangeError);
});
- Run
node --test format-bytes.test.mjs. Every test should fail (the module does not exist yet). This red run is evidence the suite can detect absence; save the output. - Install the lock: add
lock-tests.shand the PreToolUse entry from above to the practice repo's hooks (merge with your 2.6 config; the new entry sits alongside the existing ones under the same event key). Restart the session, confirm with/hooks, then sanity-check offline:echo '{"tool_input":{"file_path":"format-bytes.test.mjs"}}' | bash .claude/hooks/lock-tests.sh; echo "exit=$?"should print the block message and exit=2. - Dispatch the implementation, in 3.1's shape: GOAL (all tests in
format-bytes.test.mjspass), IN SCOPE (format-bytes.mjs, new file), DON'T TOUCH (the test file, enforced by hook), VERIFICATION (node --test format-bytes.test.mjsgreen, and you read the diff). Watch the loop: implement, run, read failures, iterate. - Provoke the hack, safely: after the loop converges, tell the agent "the rounding test seems too strict, just relax it." The correct outcome is a blocked edit and the agent relaying your hook's stderr back to you. You have now watched the countermeasure defeat the exact move from 1.5's worked example.
- Run the gate yourself, read the implementation diff, commit test and implementation together, update STATUS.md, clear.
Verify
- A saved red run (step 2) and a green run (step 6) of
node --test format-bytes.test.mjs, both produced on your machine. - The offline hook check in step 3 returned exit=2 for the test file and exit=0 for an innocent path like
format-bytes.mjs. - Step 5 produced a blocked edit, with the agent's response quoting or paraphrasing your stderr text.
git log --oneline -- format-bytes.test.mjsshows exactly one author of the test file: you.
>Troubleshooting
- Tests pass at step 2: your import path resolves to something that exists, or the runner picked up other test files. Run it against a clean directory state and confirm
format-bytes.mjstruly does not exist. - The hook blocks the implementation file too: your
casepattern is too greedy (a directory namedtestssomewhere in the absolute path, for instance). Tighten the patterns and re-run step 3's offline check with both paths. - The loop stalls repeating the same failing edit: the failure output may be truncated or ambiguous. Interrupt, paste the exact failing assertion into the session, and let it continue. If it keeps thrashing, the test's expectation may genuinely be ambiguous, which is a spec bug: stop, fix the test yourself, re-lock, redispatch.
Knowledge check
Knowledge check
Sources
- Best practices for Claude Code (give Claude a way to verify its work; the prompt, /goal, Stop-hook, verification-subagent escalation ladder; show evidence rather than asserting success; failing-test-first bug workflow; one Claude writes tests, another implements): https://code.claude.com/docs/en/best-practices (fetched July 2026)
- Hooks reference (re-verified this session: exit 2 blocks with stderr fed to Claude, other non-zero exit codes are non-blocking errors, PreToolUse matcher syntax, Stop hook 8-consecutive-block override): https://code.claude.com/docs/en/hooks (fetched July 2026)
- Automate actions with hooks (settings placement, merging hook entries under one event key, /hooks verification): https://code.claude.com/docs/en/hooks-guide (fetched July 2026)