jared hebb ~ %

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-purpose Explore Plan

What it receives

The event arrives as JSON on standard input. These are the fields worth reading.

FieldTypeWhat it is
agent_typestringWhich kind of subagent finished.
agent_idstringIdentifies this particular subagent.
last_assistant_messagestringThe report it is about to return.
session_idstringThe parent session.
cwdstringWorking 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.

ExitWhat happens
0Let the subagent finish.
2Do not stop. The subagent continues, and stderr tells it why.
otherNon-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.