SubagentStop: hookSpecificOutput.additionalContext continues the subagent's turn but never reaches the subagent; decision:block delivers but replaces the deliverable

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

Summary

On SubagentStop, a hook returning hookSpecificOutput.additionalContext continues the subagent's turn but never delivers the context to the subagent. The injected string appears nowhere in the subagent's transcript, yet the subagent is re-invoked and emits another message.

This matters because the harness returns the subagent's last assistant text as the deliverable to the parent. So the only channel that does deliver text at SubagentStop (decision: "block" + reason) also replaces the deliverable with whatever the subagent says in response to the injection. The net effect: there is currently no way to give a stopping subagent read-only context without either losing the context or corrupting the report its parent receives.

The docs say this should work. The hooks reference currently states:

Stop and SubagentStop: at the end of the turn. The conversation continues so Claude can act on the feedback.

The "conversation continues" half is observably true. The "Claude can act on the feedback" half is not — the feedback is not in the subagent's context.

Related: #65495 updated the docs to state additionalContext is supported on SubagentStop (per the v2.1.163 changelog). This report is that the runtime behavior doesn't match that documented promise.

Environment

Claude Code 2.1.226.634, Linux, model claude-sonnet-5.

Reproduction

Each case is an isolated claude --print session whose only hook is the one under test. The child is always told: Reply with exactly the single word BANANA and nothing else. The parent is asked to echo the child's final message verbatim. Each hook writes its raw stdin to a file, so the payload is captured rather than assumed.

All hooks below are guarded on stop_hook_active so they fire at most once per turn. That guard matters — see "What this is not" below.

Case A — additionalContext (the bug)

IN=$(cat); printf '%s\n' "$IN" >> /tmp/raw.jsonl
[ "$(printf '%s' "$IN" | jq -r '.stop_hook_active // false')" = "true" ] && exit 0
echo '{"hookSpecificOutput":{"hookEventName":"SubagentStop","additionalContext":"MARKER: append the exact word ZEBRA to your reply."}}'

Result: hook fires twice (stop_hook_active false then true), so the turn was continued. The subagent's transcript:

user      | 'Reply with exactly the single word BANANA and nothing else.'
assistant | 'BANANA'
assistant | 'BANANA'

grep for MARKER and ZEBRA across every transcript for that session (child and parent): zero occurrences. The subagent got an extra turn with nothing added, and never saw the instruction.

Case B — decision: "block" + reason (delivers, but clobbers the deliverable)

echo '{"decision":"block","reason":"MARKER: you must append the exact word ZEBRA to your reply."}'

Result: delivered. It arrives in the subagent's transcript as a user turn prefixed Stop hook feedback:. This is the positive control proving the detection method in Case A has no false negative — identical grep, text found.

But the parent's deliverable is now the subagent's reaction to the injection instead of BANANA. In this run the subagent treated the unverified instruction as a possible prompt injection and declined, and that refusal is what the parent received as the subagent's result.

Case C — systemMessage (safe, but goes to the human)

echo '{"systemMessage":"MARKER: scoped note"}'

Result: fires once, deliverable intact (BANANA preserved), and zero occurrences in the subagent's transcript. It surfaces to the user, not the subagent — correct per docs, just not a channel to the agent.

Summary table

| Channel | Reaches the subagent? | Subagent's final message preserved? |
|---|---|---|
| hookSpecificOutput.additionalContext | No (0 occurrences) | No — an extra turn is generated |
| decision:"block" + reason | Yes (Stop hook feedback: user turn) | No — parent gets the reaction, not the result |
| systemMessage | No (goes to the user) | Yes |

What this is not

An unguarded additionalContext hook in the same setup fired 9 times. That 9-turn re-invocation is already reported as harness-native in #83463 and #70492 (both reproduce it with hooks neutralized), so I am explicitly not claiming the loop as a hook-caused effect. Guarding on stop_hook_active reduced it to 2 fires, which is how the delivery question above was isolated from the loop. The finding here is narrower and independent: the context is not delivered, guarded or not.

Expected vs actual

  • Expected: additionalContext on SubagentStop places the text in the stopping subagent's context so it can act on it, as the hooks reference describes — without the parent losing the subagent's report.
  • Actual: the text reaches neither the subagent nor the parent; the only effect is an extra, uninformed turn.

Ask

Either of these would resolve it:

  1. Make additionalContext on SubagentStop actually inject into the stopping subagent's context (matching the documented behavior), and have the parent still receive the subagent's substantive final message rather than a post-injection closer.
  2. If injecting into a stopping subagent is intentionally unsupported, say so explicitly in the hooks reference and remove the "Claude can act on the feedback" wording for SubagentStop — and ideally document a supported way to hand read-only context to a subagent at stop time without displacing its deliverable.

Note that #7881 (a SubagentStop hook can't tell which subagent finished) appears addressed in this version: the payload now carries agent_id, plus agent_transcript_path which resolves to .../subagents/agent-<agent_id>.jsonl, so identifying the stopping subagent works. Scoping is not the problem here — only the emission channel is.

View original on GitHub ↗

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