CCLearn

The escalation ladder — prompt, /goal, Stop hook, second opinion

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

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

RungWho decides the turn is overReach for it whenSetup
1 — check in the promptClaudeYou are watching and will read the outputNone
2 — a /goal conditionA separate evaluator, after every turnLong unattended stretch, and Claude's own output can prove the end stateOne command
3 — a Stop hookYour script's exit codeThe check must hold for every session in the projectA settings file and a script
4 — a second opinionA model that did not write the codeThe worry is a wrong result, not an unfinished oneA subagent or a workflow

Each step trades setup for attention. The prompt version works on any task today. The /goal and 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 /goal command 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

text
/goal all tests in test/auth pass and the lint step is clean

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

PartExample
One measurable end stateA test result, a build exit code, a file count, an empty queue
A stated checkHow Claude should prove it — npm test exits 0, git status is clean
Constraints that matterWhat 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."

/goalA Stop hook
FiresAfter every turnAfter every turn
Lives inThis session onlyYour settings file, for every session in its scope
Judged byA small fast model reading the transcriptYour script's exit code, or a prompt, or an agent
Set byTyping a commandEditing .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.

bash
#!/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:

json
{
  "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 returnsWhat Claude getsIn the transcript
Exit 2, reason on stderrThe stderr text, as why it should continueA blocking error
decision: "block" with reasonThe reason — required when blockingA blocking error
hookSpecificOutput.additionalContextThe text, as guidanceStop 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

TypeWhat it isReach for it when
commandYour script, judged by its exit code or the JSON it printsThe check is deterministic. This is the production gate
promptA single model call over the hook's inputThe hook input alone is enough to decide
agentA subagent that can read files and run commandsThe 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

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

    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 test exits 0, a search for the old API name returns no matches outside the vendored directories, and no file outside src/ 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.

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

    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 — true whenever 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 is CLAUDE_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 as additionalContext buys no rope either — same stop_hook_active input, same eight-continuation cap. The script has to stop blocking on a finding Claude is not allowed to fix.

  3. /goal and 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 answer

    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.json or ~/.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: /goal is one command typed now, against a settings file plus a script you have to remember to chmod +x. The scope that makes the hook powerful also cuts against it for a one-off — Stop hooks 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.

  4. Your gate must confirm that no file outside src/api changed. Command, prompt or agent hook — and why is the answer not the one that sounds smartest?

    Show answer

    command. The check is deterministic — a script can list the changed paths and exit 2 if any of them sit outside src/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. A prompt hook 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.

Sources