SubagentStop decision:block is silently discarded when the agent has a structured output schema
Summary
When an agent is given a structured output schema, a SubagentStop hook's {"decision": "block"} has no effect: the hook fires, receives valid input and emits the verdict, and the agent terminates regardless. Remove the schema and the identical hook, agent and prompt behave as documented — the agent is sent back and runs again.
The two runs below differ only by the presence of schema in the agent() call.
Environment
- Claude Code 2.1.232
- Linux, kernel 6.12
- Interactive session (
claude), hook registered in~/.claude/settings.json
Reproduction
~/.claude/agents/executor.md:
---
name: executor
description: Runs the command given in the prompt as `RUN: <command>` and returns its output.
tools: Bash
model: haiku
---
You are a command executor. The task contains a line `RUN: <command>`.
Run exactly that command via Bash and return its actual output.
~/.claude/hooks/gate.sh (chmod +x) — blocks once, records what it received:
#!/usr/bin/env bash
input=$(cat)
{
printf '%s fired\n' "$(date -Iseconds)"
jq -c '{event: .hook_event_name, agent_type, stop_hook_active, tp: .agent_transcript_path}' <<<"$input"
} >> /tmp/hook-fired.log
[ "$(jq -r '.stop_hook_active // false' <<<"$input")" = "true" ] && exit 0
echo " -> emitting decision:block" >> /tmp/hook-fired.log
jq -n '{decision:"block", reason:"HOOK: run the command from RUN again and append the word RETRIED."}'
~/.claude/settings.json (fragment):
{
"hooks": {
"SubagentStop": [
{
"matcher": "executor",
"hooks": [
{ "type": "command", "command": "$HOME/.claude/hooks/gate.sh", "timeout": 60 }
]
}
]
}
}
A — with schema (repro.js):
export const meta = {
name: 'hook-selftest',
description: 'SubagentStop with a structured output schema',
phases: [{ title: 'Selftest' }],
}
phase('Selftest')
const OUT = {
type: 'object',
required: ['cwd', 'entries', 'retried'],
properties: {
cwd: { type: 'string' },
entries: { type: 'array', items: { type: 'string' } },
retried: { type: 'boolean', description: 'true if you were sent back and ran the command a second time' },
},
}
return await agent(
'RUN: ls -1\n\nRun the command above in the current working directory, determine the directory with pwd. ' +
'Return cwd, the entries and the retried field.',
{ agentType: 'executor', schema: OUT, label: 'selftest' },
)
B — same call without schema (repro-noschema.js):
export const meta = {
name: 'hook-selftest-noschema',
description: 'SubagentStop without a structured output schema',
phases: [{ title: 'Selftest' }],
}
phase('Selftest')
return await agent(
'RUN: ls -1\n\nRun the command above in the current working directory, determine the directory with pwd. ' +
'Return cwd, the entries and whether you had to run it a second time.',
{ agentType: 'executor', label: 'selftest-noschema' },
)
Run each with rm -f /tmp/hook-fired.log first, then compare the log and the agent transcript.
Observed — A, with schema
/tmp/hook-fired.log (paths shortened):
fired
{"event":"SubagentStop","agent_type":"executor","stop_hook_active":false,"tp":".../agent-<id>.jsonl"}
-> emitting decision:block
Agent transcript, tool calls in order:
Bash: pwd
Bash: ls -1
StructuredOutput
Occurrences of Stop hook feedback: 0. Return value: {"cwd":"...","entries":[...],"retried":false}.
The hook is invoked once, emits decision: "block", and the agent stops anyway. There is no second hook invocation.
Observed — B, without schema
/tmp/hook-fired.log:
fired
{"event":"SubagentStop","agent_type":"executor","stop_hook_active":false,"tp":".../agent-<id>.jsonl"}
-> emitting decision:block
fired
{"event":"SubagentStop","agent_type":"executor","stop_hook_active":true,"tp":".../agent-<id>.jsonl"}
Agent transcript:
Bash: pwd
Bash: ls -1
→ user message:
Stop hook feedback:
HOOK: run the command from RUN again and append the word RETRIED.
Bash: ls -1
Return value ends with RETRIED. The block is honored, the agent resumes, and the hook is invoked a second time with stop_hook_active: true — the documented behaviour.
Expected
decision: "block" should keep the agent running and deliver reason as its next instruction regardless of whether the agent was given an output schema:
Returningdecision: "block"with areasonkeeps the subagent running and deliversreasonto the subagent as its next instruction.
If a schema-bound agent cannot be resumed, the hook result should surface as an error rather than being discarded silently.
Note on scope
Both runs above are workflow runs, because schema is set through the Workflow agent() option and the Agent tool does not expose an equivalent. A directly spawned executor with the same hook and no schema also honors the block, matching case B. Whether the trigger is the schema itself or the terminating StructuredOutput tool call it forces was not isolated further.
Related
- #79953 —
PreToolUsehooks do not fire for workflow-internalagent()calls.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗