SubagentStop hook: missing agent_type + orphan Stop events without paired Start

Resolved 💬 5 comments Opened May 16, 2026 by bettep-dev Closed Jun 21, 2026

Summary

The SubagentStop hook exhibits two related defects that together break
downstream attribution and metrics pipelines:

  1. Payload field omission: when SubagentStop fires, the JSON payload

delivered to the hook script omits the resolved agent_type (the
specialized agent name passed as subagent_type at delegation time).
The payload includes agent_id, session_id, transcript_path,
cwd, hook_event_name, etc., but the type-name field is missing.

  1. Orphan Stop events: a significant fraction of SubagentStop

events fire with no preceding SubagentStart event of any kind,
no on-disk sidecar, and no corresponding Agent tool_use in the
parent session JSONL. This suggests an internal spawn path that
bypasses the documented Agent tool. For these orphan events the
agent_type is not just missing from the payload — it has never
been recorded anywhere on disk, making post-hoc recovery impossible.

Adding agent_type as a first-class payload field would resolve defect
#1. Defect #2 appears to be a separate event-pairing issue that may share
the same root cause or may be independent — we surface it here because
it was discovered during the same investigation.

Reproduction

  1. Register a SubagentStop hook in ~/.claude/settings.json that

dumps the hook input to a log file. Minimal hook command:
``sh
cat > /tmp/subagent-stop-payload.json
``

  1. Trigger a subagent via the Agent tool with an explicit

subagent_type, for example subagent_type: "researcher".

  1. Wait for the subagent to terminate, then inspect

/tmp/subagent-stop-payload.json.

Observed (defect #1): payload contains session_id, agent_id,
transcript_path, cwd, hook_event_name, but no agent_type (or
subagentType) field.

Observed (defect #2): in addition to defect #1, some sessions emit
SubagentStop events whose agent_id does not appear in any
SubagentStart event, in any .meta.json sidecar, or in any parent
JSONL Agent tool_use entry. Defect #2 is not reliably reproducible via
standard Agent tool invocations — see "Internal investigation findings"
below.

Expected behavior

  • The hook payload includes the resolved agent type string, matching

the subagent_type argument used at delegation time. Either
snake_case (agent_type) or camelCase (subagentType) is acceptable.

  • Every SubagentStop event is preceded by a matching SubagentStart

event, and either the hook payload or the on-disk sidecar carries
enough metadata to attribute the outcome to a specific agent type.

Actual behavior

Defect #1 — payload omission

The information IS persisted internally for normally-spawned subagents.
Claude Code writes a per-subagent sidecar at:

~/.claude/projects/<project>/<session>/subagents/agent-<agent_id>.meta.json

whose agentType field carries the type name. We audited 3,300
historical sidecars from our local installation and 3,299 of them
(99.97%) had the agentType field populated. The sidecar schema is
stable ({agentType, description}, optionally name — single-line
JSON, 52-127 bytes) with no observed schema drift over a six-week
window. The data exists in the persistence layer; it is simply not
propagated to the hook payload.

Defect #2 — orphan Stop events (new finding)

When we attempted to backfill historical broken outcomes by joining
on-disk sidecars and parent JSONL Agent tool_use entries, we found
that a recent population of broken rows is disjoint from the
audited 3,300 sidecars — these broken rows have no sidecar at all.

Quantitative evidence (30-day window, single user installation):

  • SubagentStop events total: 3832
  • SubagentStart events total: 2793
  • Unpaired Stop events: 1039 (34.7% orphan rate)
  • Broken outcome rows with placeholder agent type in the same window:

224 (100% of recent broken rows fall into this phantom Stop
pattern, not the missing-payload-field pattern)

  • Sidecar .meta.json present for orphan agent_ids: 5 / 224 (2.2%)

Parent JSONL Agent tool path miss:

  • Orphan agent_ids sampled (n=50): matched in any parent JSONL

message.content[i].input.subagent_type block on name="Agent"
tool_use: 0 / 50 (0%)

  • Normal (non-broken) agent_ids sampled (n=5): matched in parent

JSONL: 5 / 5 (100%)

This asymmetry indicates the documented Agent tool path is not the
spawn mechanism used for these phantom subagents. The hooks pipeline,
the on-disk sidecar writer, and the SubagentStart event recorder are
all bypassed for this population — only the SubagentStop emission
reaches downstream consumers, and it carries no agent_type metadata.

Day-cluster distribution

Orphan Stop events are not uniformly distributed across the 30-day
window:

  • 2026-05-14: 73 occurrences
  • 2026-05-11: 43 occurrences
  • 2026-05-10: 39 occurrences

The clustering on specific days suggests workload-correlated triggering
rather than random noise — consistent with an internal-only spawn path
that activates under specific conditions (possibly orchestrator
self-spawn, internal task delegation, or recursive tool invocation).

Naming consistency note (minor / non-blocking)

The sidecar files use agentType / agentId (camelCase), the
SubagentStop hook payload uses agent_id (snake_case), and Python-style
configuration in user-facing settings.json tends toward snake_case.
This is a secondary friction point — if you can normalize during the
fix, even better, but please don't let it block the primary additions.

Impact

Without these defects addressed, hook scripts performing attribution-
based metrics or learning aggregation have two unattractive options:

  1. Disk-read fallback (helps defect #1 only): on every

SubagentStop fire, join the payload's (session_id, agent_id)
against the sidecar path and parse .agentType. Cheap individually
(52-127 bytes), but adds I/O to a path that should be in-memory, and
has no effect on defect #2 because the sidecar does not exist.

  1. Drop the signal: bucket the outcome as unattributed and lose the

type-level breakdown entirely.

We measured 28.96% (223/770) of outcomes in a recent 7-day window
landing in the unattributed bucket. Of the recent 30-day broken rows
(224 in total), 0% are recoverable post-hoc — no sidecar, no parent
tool_use, no SubagentStart event exists anywhere on disk.

A cumulative count from the earlier observation window (outcome-record.sh
inline comment) reported 956+ unattributed outcomes prior to our
sidecar-based workaround. Top affected types in our installation (by
raw subagent count, normally-spawned population): react-dev (499),
reporter (353), planner (297), prompt-engineer (293), nestjs-dev
(244), python-dev (230), android-dev (215), shell-dev (211),
researcher (188), nodejs-dev (152).

Workaround

For anyone hitting defect #1 today: hook scripts can join the payload's
(session_id, agent_id) against the sidecar path

~/.claude/projects/<project>/<session>/subagents/agent-<agent_id>.meta.json

and extract .agentType. The path is deterministic and the file is
small, so the disk read is cheap.

Defect #2 has no client-side workaround — the data does not exist
anywhere on disk for orphan Stop events. Recovery requires fixing the
upstream spawn path so that it emits SubagentStart + sidecar before
the matching SubagentStop.

Requests

  1. Primary: include the resolved agent_type (or subagentType)

in the SubagentStop hook payload as a first-class field. This
eliminates the dependency on sidecar file layout staying stable
across versions, which is the main motivation for this request.

  1. Secondary: investigate why some SubagentStop events fire

without a paired SubagentStart (the orphan / phantom-Stop
population described above). This may share a root cause with #1 or
may be a separate event-pairing defect — we are happy to provide
additional diagnostic data on request.

Environment

  • Claude Code version: <please fill in: claude --version output>
  • OS: <please fill in: macOS / Linux / Windows + version>
  • Hook configuration location: ~/.claude/settings.json
  • Observation window for orphan-Stop statistics: 30 days ending

2026-05-16, single user installation

  • Observation window for sidecar audit: 6 weeks ending 2026-05-16,

3,300 sidecars across 165 sessions

View original on GitHub ↗

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