SubagentStop decision:block is silently discarded when the agent has a structured output schema

Status Open
Reported on v2.1.232
Maintainer reply None cached
Activity 1 comment · opened Aug 14, 2026

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:

Returning decision: "block" with a reason keeps the subagent running and delivers reason to 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 — PreToolUse hooks do not fire for workflow-internal agent() calls.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗