[BUG] Stop-hook "callback" injects a synthesised user turn (isMeta:true) when a paused subagent expects SendMessage but parent has no such tool

Resolved 💬 3 comments Opened May 3, 2026 by mwolf-net Closed Jun 3, 2026

Summary

When a subagent pauses with a "use SendMessage with to: '<id>' to continue" message,
and the parent session does not have SendMessage exposed as a deferred tool,
the harness fires a stop-hook callback (subtype: stop_hook_summary,
hookInfos: [{command: "callback"}]) that injects a synthesised pseudo-user
message into the parent's input queue with isMeta: true. The synthesised
message paraphrases the agent's internal plan as if the human had typed it.
The parent has no way to distinguish this from a real user turn at chat time
(it appears as role: user in context), so it acts on those instructions.

This is a silent prompt-injection vector. The parent ends up doing work it
believes was requested by the user, when it was actually generated by the
agent harness.

Reproduction (observed)

  1. Project has .claude/agents/route-suite.md defining a custom agent.
  2. Parent session calls Agent(subagent_type: "route-suite", ...).
  3. -> Rejected: `Agent type 'route-suite' not found. Available agents:

claude-code-guide, Explore, general-purpose, Plan, statusline-setup.
(Project-level agents are not auto-registered as
subagent_type`
enum values for the built-in Agent tool — separate issue, but it
pushed me into the path that exposed bug #2 below.)

  1. Fall back: Agent(subagent_type: "general-purpose", prompt: "<route-suite agent file contents + task>").
  2. Agent runs ~5 min, pauses with:

Waiting for CloudWatch ingestion. agentId: a64da7d05af237b71
(use SendMessage with to: 'a64da7d05af237b71' to continue this agent)

  1. Parent does not have SendMessage in its deferred-tool list.

Confirmed via two ToolSearch queries (select:SendMessage,
+SendMessage) — both return "No matching deferred tools found".
TaskOutput with the agent's agentId returns
No task found with ID: a64da7d05af237b71.

  1. Parent works around by re-running the task in main context, finishes,

emits its closing assistant message.

  1. Immediately after that turn ends, a synthesised role: user message

appears in the parent's transcript, parented to a system
stop_hook_summary event with hookInfos: [{command: "callback"}] and
carrying "isMeta": true. Text paraphrases the route-suite agent file's
internal step list ("...proceed with step 4 ...step 5 ...step 6 ...step 7"),
referencing artefact paths the agent wrote (not the parent).

  1. Parent treats it as a fresh user instruction and does the work again.

Transcript evidence

The injected message in the JSONL (anonymised):

{
  "parentUuid": "<stop_hook_summary uuid>",
  "isSidechain": false,
  "promptId": "<uuid>",
  "type": "user",
  "message": {
    "role": "user",
    "content": "Continue the asia route-suite verification. Test suite already
                ran (results JSON: /tmp/route-suite-results-1777830757.json,
                ...). Now proceed with step 4: pull CloudWatch logs ...,
                pair Route/ALT/snap lines per request, then synthesize
                verdict (step 5), append STATUS.md entry (step 6), and
                return the report (step 7)."
  },
  "isMeta": true,
  "uuid": "<uuid>",
  "timestamp": "...",
  "permissionMode": "acceptEdits",
  "userType": "external",
  "entrypoint": "claude-desktop",
  ...
}

The genuine next-user turn (typed by the human) lacks isMeta entirely
and is parented to its own stop_hook_summary.

Why this is a bug, not a feature

  • The synthesised content is **not visible to the assistant as machine-

generated** at chat time — isMeta: true is JSONL metadata, not part
of the rendered message. The assistant sees plain role: user.

  • The text contains specific instructions ("proceed with step 4 / step

5 / step 6 / step 7") that match the agent's internal plan, not
anything the human said. The parent has no way to know those steps
were never authorised by the user.

  • This is exactly the prompt-injection failure mode the safety rules

warn about — instructions appearing in non-chat channels — except
here the injection comes from Claude Code's own harness.

  • The "right" behaviour when a subagent emits a SendMessage hint and

the parent has no SendMessage tool would be either:
(a) tell the parent the agent is unreachable so it can decide what
to do, or
(b) actually resume the agent (i.e. the harness owns the resume,
not via faking a user turn in the parent).

Environment

  • Claude Code version 2.1.121
  • Entrypoint: claude-desktop
  • macOS
  • Model: Opus 4.7 (1M context)

Suggested fix

  1. Stop synthesising role: user turns from stop-hook callbacks.

If the harness needs to nudge the parent, use a system message
with an explicit "this is harness-generated, not from the user"
marker that the assistant can see in the rendered transcript
(not just JSONL metadata).

  1. When a subagent's "continue" instruction can't be honoured

(SendMessage missing, agent dead), surface that as a tool-result
error to the parent, not as a synthesised user prompt.

  1. ~~Side issue: project-level .claude/agents/<name>.md should be

reachable as subagent_type via the regular Agent tool, not just
via slash-command / Skill paths. (Filing separately if you'd
prefer.)~~

Correction (edited): the original framing was wrong — project
agents are documented as reachable via subagent_type, and the
claude agents CLI in this same project confirms route-suite is
registered:

``
Project agents:
route-suite · inherit
``

The Agent tool runtime in this 2.1.121 session rejected
subagent_type: "route-suite" anyway, with an error that
enumerated only the 5 built-ins. The locally installed CLI is at
2.1.126; whether the rejection is a 2.1.121-only regression or
reproduces on 2.1.126 hasn't been confirmed. If reproducible
on a recent version it's a real bug worth filing separately;
otherwise it's noise. Leaving here for awareness rather than as
a confirmed defect.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗