Claude Code hooks
The SubagentStop hook in Claude Code
SubagentStop fires when a subagent finishes and can send it back to work. The same infinite-loop trap as Stop applies.
Same shape as Stop, aimed at a subagent instead of the top-level session. The report a subagent is about to hand back is the one thing worth checking here, since a parent session that trusts an empty or vague report has no way to find that out until much later in the conversation.
- When it fires
- When a subagent finishes.
- Can it block?
- Yes. Exit 2, or decision block, keeps the subagent working.
- Matcher
- Yes, on
agent_type - Matcher examples
general-purposeExplorePlan
What it receives
The event arrives as JSON on standard input. These are the fields worth reading.
| Field | Type | What it is |
|---|---|---|
agent_type | string | Which kind of subagent finished. |
agent_id | string | Identifies this particular subagent. |
last_assistant_message | string | The report it is about to return. |
session_id | string | The parent session. |
cwd | string | Working directory. |
Exit codes
The exit code is the decision. Anything the script writes to standard error on a blocking exit is what Claude gets told.
| Exit | What happens |
|---|---|
0 | Let the subagent finish. |
2 | Do not stop. The subagent continues, and stderr tells it why. |
other | Non-blocking error. |
What it can return
Same shape as Stop. Blocking sends the subagent back with your reason attached, which is how you insist on a report format or refuse an empty result.
{
"decision": "block",
"reason": "string",
"hookSpecificOutput": {
"hookEventName": "SubagentStop",
"additionalContext": "string"
}
}
Refusing a report with no evidence in it
A search subagent that returns a conclusion and no file paths has not done the job. Checking the last message for at least one concrete reference costs nothing and catches the empty answer before it reaches the parent.
In settings.json
{
"hooks": {
"SubagentStop": [
{
"matcher": "Explore",
"hooks": [
{ "type": "command", "command": "node ~/.claude/hooks/require-evidence.mjs" }
]
}
]
}
}
The script
const input = JSON.parse(require('fs').readFileSync(0, 'utf8')) || {};
const msg = input.last_assistant_message || '';
if (/[\w./-]+\.(js|ts|py|md):\d+/.test(msg)) process.exit(0);
console.error('Cite at least one file and line for what you found.');
process.exit(2);
Match a specific agent_type so the rule applies where it makes sense rather than to every subagent.
The catch
As with Stop, a blocking hook here needs a condition under which it gives up, or the subagent never returns and the parent waits forever.