SubagentStop hook: missing agent_type + orphan Stop events without paired Start
Summary
The SubagentStop hook exhibits two related defects that together break
downstream attribution and metrics pipelines:
- Payload field omission: when
SubagentStopfires, 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.
- 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
- Register a
SubagentStophook in~/.claude/settings.jsonthat
dumps the hook input to a log file. Minimal hook command:
``sh``
cat > /tmp/subagent-stop-payload.json
- Trigger a subagent via the Agent tool with an explicit
subagent_type, for example subagent_type: "researcher".
- 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 (orsubagentType) field.
Observed (defect #2): in addition to defect #1, some sessions emitSubagentStop events whose agent_id does not appear in anySubagentStart 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
SubagentStopevent is preceded by a matchingSubagentStart
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):
SubagentStopevents total: 3832SubagentStartevents 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.jsonpresent 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), theSubagentStop 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:
- 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.
- 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
- Primary: include the resolved
agent_type(orsubagentType)
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.
- Secondary: investigate why some
SubagentStopevents 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 --versionoutput> - 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
This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗