SubagentStop hook does not fire when team agents terminate via shutdown protocol
Summary
When a team agent (teammate) terminates via the shutdown_request → shutdown_approved protocol, the SubagentStop hook does not fire. This leaves external systems that track agent lifecycle via hooks with phantom agent counts that never decrement.
Reproduction
- Enable agent teams:
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 - Register a
SubagentStophook that logs to a JSONL file (or any observable side effect) - Create a team and spawn teammates
- Send
shutdown_requestto teammates - Observe that
shutdown_approvedis received,teammate_terminatedsystem message appears, butSubagentStophook never fires
Evidence
JSONL event log showing 6 team agents with agent.start events and no matching agent.stop:
# Team 1: resilience-architect and test-architect
{"ts":"2026-04-08T01:54:13Z","event":"agent.start","agent_id":"aacd2d977d5b10367","agent_type":"resilience-architect"}
{"ts":"2026-04-08T01:54:26Z","event":"agent.start","agent_id":"ad43c61987f3d60bd","agent_type":"test-architect"}
# Both received shutdown_approved — no agent.stop events ever appeared
# Team 2: three review agents
{"ts":"2026-04-08T02:09:22Z","event":"agent.start","agent_id":"a712f607506a003a7","agent_type":"reuse-reviewer"}
{"ts":"2026-04-08T02:09:22Z","event":"agent.start","agent_id":"a912c5cfbfc5e9ae4","agent_type":"quality-reviewer"}
{"ts":"2026-04-08T02:09:23Z","event":"agent.start","agent_id":"ab21e0518d3daf119","agent_type":"efficiency-reviewer"}
# All three received shutdown_approved — no agent.stop events
Meanwhile, regular subagents (non-team) and team agents that go through idle→wake cycles (via SendMessage) DO correctly fire SubagentStop. The issue is specifically the shutdown_request → shutdown_approved termination path.
Expected behavior
SubagentStop should fire for every agent that triggered SubagentStart, regardless of how the agent terminates (normal completion, shutdown protocol, or error).
Impact
Any plugin or hook that tracks agent count via SubagentStart/SubagentStop pairs will show permanently inflated counts after team sessions. This affects resource management tools that gate behavior on active agent count.
Environment
- Claude Code v2.1.94
- macOS Darwin 25.3.0
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
Context
Discovered while building coolant, a resource management plugin that tracks agent lifecycle via hooks to drive a thermal monitoring dashboard. The breathing agent icons stay lit indefinitely after team agent shutdown because the stop events never arrive.
12 Comments
Found 1 possible duplicate issue:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Duplicate of #27755 and #25147 — the missing SubagentStop on team agent shutdown is a subset of the broader issue where subagent lifecycle hooks don't fire on completion. Closing in favor of those.
Our workaround in coolant: agents that exceed a staleness threshold (3 min with no stop event) are visually dimmed in the dashboard rather than removed — orphaned but honest.
Reopening — this is not a duplicate of #27755 or #25147. The cited issues describe different failure modes:
#25147 — Background agents bypass Stop hooks
Stop(session-level), notSubagentStoprun_in_background=true#27755 — SubagentStart/SubagentStop unreliable for Task tool
SubagentStart/SubagentStopviasettings.jsonTasktool dispatchesagent_typesometimes emptyThis issue (#44971) — SubagentStop not fired on team agent shutdown protocol
SubagentStop(works perfectly for non-team agents)shutdown_request→shutdown_approvedKey distinction: Our
SubagentStart/SubagentStophooks fire reliably and symmetrically for every regular subagent (Agent tool). We have JSONL evidence of dozens of clean start/stop pairs in the same session. The failure is isolated to one specific termination path: the team agent shutdown protocol.Evidence from a single session showing both working and broken hooks:
This is a team-specific code path issue, not the general SubagentStop unreliability described in #27755.
Additional finding: shutdown protocol bypass affects hook-driven state
The missing
SubagentStopon the shutdown protocol path has a compounding effect beyond missed events. Any system that tracks agent count viaSubagentStart/SubagentStoppairs — including Claude Code's own hook infrastructure — will accumulate phantom agents that never decrement.In our testing, this caused
SubagentStarthooks that read agent count to make incorrect decisions for the remainder of the session, since their count inputs were permanently inflated by terminated-but-untracked agents.The distinguishing signal for debugging: in the same session,
SubagentStopfires correctly for every Agent tool subagent that completes normally. It only fails for agents that terminate viashutdown_request→shutdown_approved. This points to a specific code path in the team agent shutdown handler that doesn't call the same hook dispatch as normal agent completion.Minimal repro narrowed to one variable:
SubagentStartfires, agent completes →SubagentStopfires ✓team_name→SubagentStartfires, sendshutdown_request, receiveshutdown_approved→SubagentStopnever fires ✗Same hook config, same session, same matcher (
.*). The only variable is the termination path.Shameless plug — coolant is the thermal dashboard that surfaced this bug. The orphaned team agents were visible as breathing hexagons that never stopped. We've since added stale-agent dimming so orphaned dots fade to ~35% brightness after 3 minutes, making the missing
SubagentStopevents immediately obvious at a glance.Your root cause analysis is spot on — the shutdown protocol handler bypasses the hook dispatcher that normal agent completion uses.
Alternative workaround: PostToolUse hook on SendMessage
Since
teammate_terminatedsystem messages do appear (as you noted), andSendMessageis the tool used to communicate with team agents, you can use aPostToolUsehook onSendMessageto detect shutdown completion and synthetically handle cleanup:This catches the termination at the moment
shutdown_approvedcomes back, rather than waiting for a staleness timeout. The tradeoff is that it depends on the response text containing recognizable shutdown keywords — your 3-minute staleness approach in coolant is more robust as a fallback.Both approaches could complement each other: the hook for immediate detection, staleness dimming for any edge cases the hook misses.
@todd-w-shaffer I identified the root cause and have a fix merging. Should be deployed early next week.
Thanks man!
ran into this same lifecycle tracking issue. when agents terminate via shutdown protocol instead of normal exit, any external monitoring that relies on stop hooks ends up with ghost agents that look alive forever.
we worked around it by adding a secondary heartbeat check - if an agent hasnt written to its heartbeat file in 120 seconds, consider it dead regardless of what the hook says. the hook is nice to have but you cant rely on it as the sole source of truth for agent lifecycle. belt and suspenders.
the deeper issue is that agent lifecycle should be tracked by the orchestrator observing process state, not by the agent self-reporting. dead agents cant report their own death.
This was fixed in v2.1.101 — The SubagentStop hook now fires when team agents are terminated via the shutdown protocol, guaranteeing it pairs with SubagentStart. If you're still seeing this in the latest version, please comment with your version and repro and we'll reopen.
Follow-up: fix verified, one adjacent issue found
Confirmed in 2.1.105: team agents fire clean per-turn
SubagentStart/SubagentStopbrackets on turn end and on SendMessage wake. Testedacross parallel agents, team initial spawn, idle/wake cycles, and
multi-session concurrency:
| Behavior | Status |
|---|---|
| Parallel agent start/stop | Reliable |
| Team initial spawn lifecycle | Reliable |
|
agent_type= teammate name | Reliable || Per-turn
agent_idrotation | Reliable || Team idle/wake hooks | Reliable (per-turn brackets clean) |
|
session_idconsistency | Reliable |One adjacent issue surfaced during follow-up testing — likely same
code-path family, separate from this fix. Detailed below.
(Separately filed #49671 for the team shutdown protocol — different
surface, not related to this fix.)
---
Adjacent bug: orphan
SubagentStopon session idle cleanup~6-11 minutes after the last agent stops in a session, CC fires a
phantom
SubagentStopwith:agent_type(zero-length string)agent_idthat matches no priorSubagentStartsession_id(correct session UUID)The empty
agent_typemakes it impossible to attribute the stop; thephantom
agent_idcreates an unpaired stop event that desyncs anystart/stop bookkeeping.
Reproduction (confirmed 2026-04-16)
Spawned 3 team agents in session
1cd8e43f. All completed withinseconds, producing clean start/stop pairs. Monitored the JSONL event
log continuously.
No further orphan events appeared in 8 minutes of additional
monitoring. One orphan per session, regardless of agent count.
Reproduction steps for any Anthropic engineer
stdin fields to a JSONL file
agent_type, freshagent_idmatching no prior start, at approximately +6-11 minutes
Does not require waiting hours.
Timer behavior
Across multiple sessions, the gap from the last real
SubagentStopto the orphan event clusters at 6-11 minutes (N=3: 6.4min, 7min,
11min). Exact timer value unknown; could be deterministic with jitter,
or a periodic sweep the agent happens to be caught by. An engineer
with codebase access should be able to identify it.
Two early samples showed 24h and 31h gaps, both explained by macOS
sleep suspending CC — the timer fires on wake.
18 orphan events observed during the full investigation, all with the
same shape (
agent_type="", freshagent_id, one per session peridle period). Full JSONL available on request.
One sample event for schema reference:
Hypothesis (needs code-side verification)
The fresh
agent_idsuggests either (a) the cleanup path creates anew agent context rather than closing an existing one, OR (b) the
cleanup path shares code with the turn-completion path but fails to
pass agent metadata (agent_id gets regenerated, agent_type gets
dropped). Both match the observed data. Someone with repo access can
tell which.
The turn-completion path the fix touched populates both fields
correctly. The session-idle cleanup path appears to be either a
separate code path or a shared one with a metadata-passing gap.
Ask
Either:
agent_typeon cleanup-path SubagentStop events (samefields as the turn-completion path), OR
session boundary be implicit, or fire a dedicated
SessionEndevent instead
---
Small feature requests enabled by the fix
Now that per-turn brackets work, these become the next gaps. Filing
inline rather than as separate issues since they're adjacent:
reasonfield on SubagentStop — distinguishturn_complete/terminated/session_cleanup. Currently allstops look identical; consumers can't tell idle-in-mailbox from
permanently-gone.
agent_typesemantics — for team members it carriesthe teammate's
name(e.g.,"alpha"), not the subagent class(e.g.,
"general-purpose"). Discovered empirically — stablecontract, or implementation detail?
session_id— consistent per-session and necessaryfor any multi-session hook consumer. Not currently documented.
Happy to file these as separate issues if preferred.
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.