Claude has finished, and you do not have control yet
Claude finishes its turn, but you do not have the keyboard back yet. Before Claude Code hands control to you, Stop runs once. It does not run when you interrupt Claude mid-response. It does not run when the turn ended because the API returned an error either. That second case fires StopFailure instead, which cannot reverse anything. There is no matcher.
What the hook can read
Your hook needs data before it can decide whether Claude should keep going. The payload carries permission_mode, stop_hook_active, last_assistant_message, background_tasks, and session_crons. Each entry in background_tasks has an identity, a type, a status, a description, and detail specific to its type. Each entry in session_crons has an identity, a schedule, a recurrence, and the prompt it runs. If you want Claude to wait for a background job, read background_tasks. Do not guess from the message text.
Sending Claude back to work
Exit 0 lets Claude stop unless the JSON says otherwise. Exit 1 also lets Claude stop, and the transcript notes the failed hook, unless valid JSON continues the conversation. Exit 2 prevents the stop and hands Claude the JSON reason when there is one, and stderr otherwise, as the instruction to continue.
The JSON has two paths. A top-level decision: "block" with a required reason frames the continuation as an error to fix. hookSpecificOutput.additionalContext under hookEventName: "Stop" continues the conversation with feedback that is not framed as an error. The lab’s tests_must_pass uses the first path and exits 0, because the JSON carries the decision.
The loop, and the number eight
Block a stop and Claude continues, finishes, and hits Stop again. On that second pass stop_hook_active is true, as it is whenever the current run is a continuation an earlier Stop hook caused. The hooks reference says Claude Code ends the turn after eight consecutive blocks, whatever the hook returns.
You write a Stop hook that blocks whenever the test suite is red, and you never check stop_hook_active. Claude finishes, the hook blocks, Claude tries to fix the tests, finishes, the hook blocks again.
If the failure is one Claude cannot fix, that goes round until Claude Code overrides the hook after eight consecutive blocks. You will not loop forever, but you will waste eight turns finding that out, and every one cost tokens and time. There is no error, because each block was a decision your hook made.
Read stop_hook_active first and exit 0 when it is true, as tests_must_pass does. The field gives you one retry. The cap gives you seven more you did not want.
Module 09 builds tests_must_pass and explains why it exits 0 while blocking.