claude-code - 💡(How to fix) Fix SubagentStop hook: missing agent_type + orphan Stop events without paired Start

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

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.
  2. 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.

Root Cause

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.

Fix Action

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.

Code Example

cat > /tmp/subagent-stop-payload.json

---

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

---

~/.claude/projects/<project>/<session>/subagents/agent-<agent_id>.meta.json
RAW_BUFFERClick to expand / collapse

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.
  2. 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:
    cat > /tmp/subagent-stop-payload.json
  2. Trigger a subagent via the Agent tool with an explicit subagent_type, for example subagent_type: "researcher".
  3. 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.
  2. 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.
  2. 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

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

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.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix SubagentStop hook: missing agent_type + orphan Stop events without paired Start