jared hebb ~ %

Claude Code hooks

The SubagentStart hook in Claude Code

SubagentStart fires when a subagent is spawned and can hand it context the parent session never passed on.

The natural comparison is SessionStart, and the difference is who receives the output: this one talks to the subagent that was just spawned, not to the parent session watching it. A rule meant to bind every subagent a session creates belongs here, since a rule that only lives in a prompt applies to the subagents someone remembered to write it into.

When it fires
When a subagent is spawned.
Can it block?
No.
Matcher
Yes, on agent_type
Matcher examples
general-purpose Explore Plan ^my-plugin:reviewer$

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. Also what the matcher filters on.
agent_idstringIdentifies this particular subagent.
session_idstringThe parent session.
cwdstringWorking directory.
transcript_pathstringPath to the transcript.

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
0Success. stdout is added as context for the subagent.
2Shown as a notice. The subagent proceeds.
otherNon-blocking error.

What it can return

additionalContext goes to the subagent, not to the parent. This is the place to enforce something on every subagent regardless of what prompt spawned it.

{
  "additionalContext": "string",
  "systemMessage": "string"
}

Making every subagent verify before it reports

A subagent's report is a claim, not a finding. If that rule only lives in the prompt the parent wrote, it applies to the subagents someone remembered to write it into. Attaching it here applies it to all of them.

In settings.json

{
  "hooks": {
    "SubagentStart": [
      {
        "hooks": [
          { "type": "command", "command": "node ~/.claude/hooks/subagent-rules.mjs" }
        ]
      }
    ]
  }
}

The script

console.log(JSON.stringify({
  additionalContext: 'Every factual claim must be backed by state you pulled ' +
    'in this session. If you cannot verify it, say so rather than asserting it.'
}));
process.exit(0);

Leave the matcher off to cover every subagent type, including ones added later.

The catch

The matcher accepts plugin-scoped names like my-plugin:reviewer. Because a string containing a colon falls outside the exact-match character set, it is treated as an unanchored regular expression, which is why the anchored form appears in the docs.