`background_tasks` never clears idle `in_process_teammate` entries, so Stop-hook consumers can't tell a finished teammate from a working one
Summary
background_tasks in the Stop / SubagentStop hook payload correctly drops one-shot tasks once they finish, but an in_process_teammate entry stays status: "running" for the entire lifetime of the session — including while the teammate is idle and waiting for a message. The payload carries no field that distinguishes an idle teammate from a busy one.
The consequence is that once a session spawns a single named teammate, background_tasks is permanently non-empty, and any hook that uses it to answer "is this session still doing work?" is permanently wrong for the rest of the session.
Version
Claude Code 2.1.228 (macOS, native install).
Reproduction
- Register a
Stophook that appends the raw payload to a file. - In a session, spawn a named teammate:
Agent(name: "probe", subagent_type: "general-purpose", prompt: "Reply with exactly the word: done.")
- Wait for the teammate to report idle.
- End several turns and inspect the captured
Stoppayloads.
Observed
The teammate entry never leaves background_tasks and never changes status, across every subsequent Stop in the session:
"background_tasks": [
{"id": "t6edmim5e", "type": "teammate", "status": "running",
"description": "Reply with exactly the word: done. Do not use any ..."}
]
This was still the payload minutes after the teammate had gone idle, with the session doing nothing at all.
By contrast, a background shell task behaves correctly — it appears while running and is gone from the next payload once it finishes:
Stop #1 bg=[teammate/running, shell/running] <- sleep 40 in flight
Stop #2 bg=[teammate/running] <- sleep 40 finished, entry removed
So the filtering itself works; it just cannot express "this teammate is alive but idle", because for a teammate status describes its lifetime, not its activity.
Expected
Some way to tell, from the payload alone, whether a listed teammate will do anything without further user input. Any of these would solve it:
- include the teammate's idle flag on the entry (e.g.
"is_idle": true), or - give idle teammates a distinct
status, or - document that
background_tasksis a "what is attached" list rather than a "what is still working" list, and expose the activity view separately.
The first is the smallest change and matches what the payload already looks like.
Impact
This breaks the exact use case background_tasks was added for in #58303 — "check if background work is still pending before allowing the agent to finish".
Our concrete case: a bridge relays a session's stops to a remote voice channel, and announces a stop only when nothing will resume the session on its own. Using background_tasks + session_crons as that signal works perfectly for shell tasks and scheduled wakeups. But a session that has ever spawned a named teammate goes permanently silent — including on the stops that mean "I am finished and I need you", which are then classified as self-resuming and suppressed.
Measured on one session: its stops were announced normally until a named teammate was spawned; from that point every one of the next 11 stops over the following 57 minutes was suppressed, with none announced. Several of those were the session sitting idle waiting for a human answer, with the teammate long finished and nothing else running.
The failure direction is the bad one: the hook silently stops firing rather than firing too often, and nothing in the payload indicates that anything is wrong.
Workaround, and why it is not great
The information does exist elsewhere: SubagentStart fires on every activation of a named teammate (not just the first spawn), and TeammateIdle fires when it goes idle, so a consumer can reconstruct per-teammate busy/idle state:
22:42:31 SubagentStart agent_type='probe-two' <- busy
22:42:33 TeammateIdle teammate_name='probe-two' <- idle
22:42:55 SubagentStart agent_type='probe-two' <- busy again (after SendMessage)
22:42:57 TeammateIdle teammate_name='probe-two' <- idle
But that forces every consumer to keep cross-event state, which the self-contained snapshot in the Stop payload was specifically designed to avoid — and that state has no recovery path if a teammate is killed without emitting TeammateIdle, or if the consumer restarts mid-session.
There is also no way to query the state instead. claude agents --json and ~/.claude/sessions/<pid>.json are both session-level and list no in-process teammates; the per-subagent .meta.json files carry identity but no status; and the peer messaging socket protocol has no roster or status request.
Related
- #58303 — the feature request that added
background_tasksto hook payloads - #60692 —
background_tasks/session_cronsare still missing from the Stop / SubagentStop hook input docs
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗