The escalation ladder — prompt, /goal, Stop hook, second opinion
By the end you can
- Place a task on the four-rung ladder by how long it runs unattended
- Write a /goal condition the evaluator can actually judge from the transcript
- Wire a Stop hook that blocks a turn on a failing check and feeds the reason back
- Handle stop_hook_active and the eight-block override deliberately
- State what /goal and a Stop hook share, and where their scope differs
- Choose between a command, prompt and agent hook for a given check
You took Lesson 5.1 seriously. The request named the test command, said to run it, and asked for the output. You went to lunch. You came back to a green summary, two failing assertions three screens up, and a sentence explaining that they look pre-existing.
The check existed. Nothing was gating on it. Claude ran it, read it, formed a view about it, and ended the turn — because ending the turn was still Claude's decision to make. That is a different problem from the one 5.1 solved, and it has its own documented answer.
Four rungs, and what each one costs you
Once the check exists, decide how hard it gates the stop: — Best practices
| Rung | Who decides the turn is over | Reach for it when | Setup |
|---|---|---|---|
| 1 — check in the prompt | Claude | You are watching and will read the output | None |
2 — a /goal condition | A separate evaluator, after every turn | Long unattended stretch, and Claude's own output can prove the end state | One command |
| 3 — a Stop hook | Your script's exit code | The check must hold for every session in the project | A settings file and a script |
| 4 — a second opinion | A model that did not write the code | The worry is a wrong result, not an unfinished one | A subagent or a workflow |
Each step trades setup for attention. The prompt version works on any task today. The
/goaland Stop hook versions are what let an unattended run finish correctly without you. — Best practices
Note what the axis is not. It is not rigour. It is how long the run goes without you looking at it.
Rung one: the check in the prompt
In one prompt: ask Claude to run the check and iterate in the same message, as in the table above. — Best practices
This is Lesson 5.1 entire, and it is the right rung most of the time. Its one limitation opened this lesson: when you are present, you are the appeal against Claude's reading of the result. When you are not, there isn't one.
Rung two: /goal
The
/goalcommand sets a completion condition and Claude keeps working toward it without you prompting each step. After each turn, a small fast model checks whether the condition holds. — Keep Claude working toward a goal
/goal all tests in test/auth pass and the lint step is cleanSetting a goal starts a turn immediately, with the condition itself as the
directive. One goal is active per session and a new one replaces the old;
/goal alone reports the condition, elapsed time, turns evaluated, token spend
and the evaluator's latest reason; /goal clear removes it. Each turn the
evaluator returns not yet met (Claude keeps working, taking the reason as
guidance), met (the goal clears), or impossible (it judges the condition
unsatisfiable, and clears the goal with a failed entry in the transcript).
There is a ceiling on how far a goal drags a stuck run. Several turns of answering the evaluator without using any tools, and Claude Code stops the loop, prints a warning, and returns control to you with the goal still set — evaluation resumes after your next prompt, so the goal is paused, not cleared.
Writing a condition the evaluator can judge
It doesn't run commands or read files independently, so write the condition as something Claude's own output can demonstrate. — Keep Claude working toward a goal
The evaluator is a model reading the transcript, with no tools and no way into your repository. A condition it can judge is one Claude's visible output settles. A condition that survives many turns has three parts:
| Part | Example |
|---|---|
| One measurable end state | A test result, a build exit code, a file count, an empty queue |
| A stated check | How Claude should prove it — npm test exits 0, git status is clean |
| Constraints that matter | What must not change on the way, such as "no other test file is modified" |
Two mechanics worth carrying: the condition can be up to 4,000 characters, so
there is room for the constraints; and to bound the run, put the bound in the
condition — or stop after 20 turns is reported against each turn and judged
from the conversation like everything else.
Rung three: a Stop hook
As a deterministic gate: a Stop hook runs your check as a script and blocks the turn from ending until it passes. Claude Code overrides the hook and ends the turn after 8 consecutive blocks. — Best practices
/goal and a Stop hook are not rival mechanisms — they are one mechanism at two
scopes. "/goal is a wrapper around a session-scoped prompt-based Stop hook."
/goal | A Stop hook | |
|---|---|---|
| Fires | After every turn | After every turn |
| Lives in | This session only | Your settings file, for every session in its scope |
| Judged by | A small fast model reading the transcript | Your script's exit code, or a prompt, or an agent |
| Set by | Typing a command | Editing .claude/settings.json or ~/.claude/settings.json |
Row three is the reason to climb: the evaluator judges what it can read, while a command hook runs the check itself, so the verdict depends on nothing the model said.
#!/bin/bash
# .claude/hooks/tests-pass.sh — block the turn while the suite is red
INPUT=$(cat)
# Already continuing because of an earlier block: let the turn end.
[ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ] && exit 0
npm test > /tmp/cc-tests.log 2>&1 && exit 0
echo "The test suite is failing. Fix it before finishing:" >&2
tail -20 /tmp/cc-tests.log >&2
exit 2"Hook scripts must be executable for Claude Code to run them" — so
chmod +x .claude/hooks/tests-pass.sh, then register it:
{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/tests-pass.sh" }] }
]
}
}/hooks browses what is configured, grouped by event; the menu is read-only, so
you add and change hooks by editing the settings file. There are three ways to
keep the turn going, and they read differently in the transcript:
| The hook returns | What Claude gets | In the transcript |
|---|---|---|
| Exit 2, reason on stderr | The stderr text, as why it should continue | A blocking error |
decision: "block" with reason | The reason — required when blocking | A blocking error |
hookSpecificOutput.additionalContext | The text, as guidance | Stop hook feedback, no hook-error notice |
A hook that blocks by exiting 2 routes the same way as
reason: Claude receives the stderr message as the explanation for why it should continue. — Hooks reference
So the reason is not decoration; it is the next instruction. Which is why
tail -20 of the real failure beats "tests failed" — Lesson 2.4's preference for
a symptom over a verdict, pointed the other way.
The block cap, and the hook that never resolves
A gate that can block is a gate that can block forever. Two defences exist, and
you have to cooperate with one of them: the cap the rung-three quote above
already named — 8 consecutive blocks, then the turn ends anyway — and the
stop_hook_active field, true whenever Claude Code is already continuing as a
result of a stop hook, which the docs say to check (or process the transcript)
rather than block on a condition that will never resolve. Lesson 9.4 takes that
guard apart, including the never-stopping session it prevents.
The early exit at the top of the script is that check, and read honestly it buys
one forced iteration before the gate stands down — the conservative shape the
docs demonstrate. Drop that line and the gate keeps pushing, with the cap as your
only backstop. If the check legitimately needs more than eight iterations to
converge, the documented move is to raise the cap with
CLAUDE_CODE_STOP_HOOK_BLOCK_CAP rather than to fight it. additionalContext
runs through the same protections — the same stop_hook_active input, the same
eight-continuation cap — so the gentler label buys no extra rope.
This is also where the /goal impossible verdict comes from: a prompt-based
Stop hook can return an impossible flag beside its reason, letting the stop
through rather than hammering at something unsatisfiable. A command hook has no
equivalent — to a script, "impossible" and "not yet" are the same exit code, and
only the cap tells them apart.
Which hook type for which check
| Type | What it is | Reach for it when |
|---|---|---|
command | Your script, judged by its exit code or the JSON it prints | The check is deterministic. This is the production gate |
prompt | A single model call over the hook's input | The hook input alone is enough to decide |
agent | A subagent that can read files and run commands | The decision needs the actual state of the codebase |
Those three rows are the guide's own split, but the bottom one carries a caveat the table cannot: "Agent hooks are experimental", and the documented advice for production workflows is to prefer command hooks. Explore with an agent hook; do not put one in a settings file your team shares.
Rung four: a second opinion
By a second opinion: a verification subagent or a dynamic workflow that checks its own findings has a fresh model try to refute the result, so the agent doing the work isn't the one grading it. — Best practices
The first three rungs make the run finish properly. This one asks whether the finished result is right. A subagent runs in its own context window, which is the entire point: the model grading the work is not the model that did it, so a wrong assumption has to survive a fresh reading to survive at all. Lesson 5.5 takes that on; authoring a subagent is Level 8. Here, rung four is invoked, not written.
Decide the rung while you write the request
The mistake is not picking the wrong rung. It is picking rung one by default and
finding out at lunch that the run needed rung two. Every rung above the first is
set up before the work — /goal before you walk away, the hook before the
session that will need it — which makes the rung part of the same decision as the
check itself. Lesson 5.1 asked what will come back failing if the change is
wrong. This lesson adds the second half: and what happens when it does.
Checkpoint
-
You set
/goal the migration is complete and the code is well tested, come back an hour later, and the goal has resolved as met — but three call sites were never migrated. Explain what the evaluator actually judged, then rewrite the condition so that outcome is not available to it.Show answerAnswer
It judged a paragraph. The evaluator is a small fast model reading the transcript; it does not run commands or read files independently, so "complete and well tested" is satisfied the moment Claude writes a confident sentence saying the migration is done — that sentence is the only evidence in the conversation. The evaluator is not gullible, you gave it nothing else to read, and three unmigrated call sites are simply invisible to a grader that cannot look at the repository.
Rewrite it with the three parts a durable condition has: one measurable end state, the check that proves it, and the constraints that matter. Set it to something like:
npm testexits 0, a search for the old API name returns no matches outside the vendored directories, and no file outsidesrc/is modified — or stop after 20 turns. Every clause there can only be satisfied by a command whose output lands in the transcript, which is exactly what the evaluator can judge. The condition can run to 4,000 characters, so there is room for the constraints, and the turn bound goes in the condition rather than in your head. -
A Stop hook runs your linter and blocks on any finding. Claude cannot satisfy it, because one finding sits in a vendored file it must not edit. What happens, after how many turns, and which field and which setting are involved?
Show answerAnswer
Claude Code overrides the hook and ends the turn after 8 consecutive blocks. So you get eight rounds of the linter running, failing, and its stderr being handed back as the reason to keep working, and then a turn that ends anyway. The gate was overridden, not satisfied, and nothing about that ending means the check passed.
The field is
stop_hook_active—truewhenever Claude Code is already continuing because of a stop hook. The docs say to check it (or process the transcript) rather than block on a condition that will never resolve, and the early exit at the top of the example script is that check: read honestly it buys one forced iteration before the gate stands down. The setting isCLAUDE_CODE_STOP_HOOK_BLOCK_CAP, the documented way to raise the cap when a check legitimately needs more than eight iterations to converge — which is the wrong reach here, because this condition cannot converge at all. A command hook has no way to say so: to a script "impossible" and "not yet" are the same exit code, and only the cap tells them apart. Relabelling the block asadditionalContextbuys no rope either — samestop_hook_activeinput, same eight-continuation cap. The script has to stop blocking on a finding Claude is not allowed to fix. -
/goaland a Stop hook fire at the same moment and can enforce the same check. Name two things the hook can do that the goal cannot, and one reason the goal is still the better choice for this afternoon's task.Show answerAnswer
The hook runs the check itself, so the verdict is your script's exit code and depends on nothing the model said, while the evaluator can only judge what it can read in the transcript. And the hook lives in your settings file —
.claude/settings.jsonor~/.claude/settings.json— so it applies to every session in its scope, where a goal is one per session, replaced by the next one and gone when the session is.The reason to stay on rung two this afternoon is setup:
/goalis one command typed now, against a settings file plus a script you have to remember tochmod +x. The scope that makes the hook powerful also cuts against it for a one-off —Stophooks fire whenever Claude finishes responding, not only at task completion, so an expensive check becomes a tax on every reply in the session including one-line answers, and it stays there for every future session and every teammate. The ladder's axis is how long the run goes without you looking at it, not rigour. -
Your gate must confirm that no file outside
src/apichanged. Command, prompt or agent hook — and why is the answer not the one that sounds smartest?Show answerAnswer
command. The check is deterministic — a script can list the changed paths and exit 2 if any of them sit outsidesrc/api— and the guide's own split gives deterministic checks to the command hook, judged by its exit code or the JSON it prints, and names it the production gate. Its verdict then depends on nothing the model said.The one that sounds smartest is
agent, because it can read files and run commands and this question is about the actual state of the codebase, which is exactly that row's trigger. But agent hooks are experimental, and the documented advice for production workflows is to prefer command hooks: explore with an agent hook, do not put one in a settings file your team shares. Aprompthook is a single model call over the hook's input, right only when that input alone settles the question — here it does not, and you would be asking a model to assert something a script can decide exactly.