[BUG] PreToolUse/PostToolUse inside a running subagent are sometimes reported under a different agent_id that has no SubagentStart, no agent_type, and never gets a SubagentStop
Preflight Checklist
- [x] I have searched existing issues. Closest: #89555, #87065, #59719, #88995 (all about
SubagentStoparriving with a freshagent_id/ emptyagent_type). This report is about a different event class:PreToolUse/PostToolUsefor tool calls of a real, running subagent arriving under anagent_idthat never appears in anySubagentStartorSubagentStopat all. - [x] This is a single bug report
- [x] I am using the latest version of Claude Code (2.1.251)
What's Wrong?
I run command hooks for SubagentStart, SubagentStop, PreToolUse, PostToolUse (matcher *), Stop, SessionStart, SessionEnd, UserPromptSubmit, Notification, PermissionRequest. Each hook forwards its stdin JSON to a local consumer that tracks subagents by agent_id: an entry is created on the first payload carrying an agent_id, agent_type is recorded whenever a payload carries it, tool_name/tool_input are recorded from PreToolUse/PostToolUse, and the entry is removed on SubagentStop with the same agent_id.
In one long-lived session (same session_id since 2026-08-06; on 2026-08-29 alone it spawned over 50 subagents with the Agent tool as asynchronous background subagents, and resumed several completed ones with SendMessage), five entries were created on 2026-08-29 between 07:20 and 16:09 UTC that:
- were created by a
PreToolUse/PostToolUsepayload (they carry atool_name), - never received any payload carrying
agent_type(so noSubagentStartever fired for thatagent_id), - never received a
SubagentStop, and are still there hours later, - three of them received payloads only for a short burst (about 1, 5 and 6 minutes) and then nothing; the other two kept receiving occasional payloads for an hour or more.
Every subagent the session really spawned (per ~/.claude/projects/<project>/<session>/subagents/agent-*.jsonl and .meta.json) got its own SubagentStart and SubagentStop under its own agent_id; all of those were matched and cleaned up normally.
Cross-referencing the stray entries' creation times and recorded tool_input against the subagent transcripts shows that the tool calls reported under the stray ids are tool calls of real, running subagents, whose other tool calls were reported under their real agent_id (with agent_type) as usual. In other words, a subagent's consecutive tool calls can be attributed to two different agent_ids, and the second id has no lifecycle events.
Cleanest case (all times UTC, 2026-08-29, ids truncated):
- Subagent
a37ed07…(agent_type: claude,spawnDepth: 1, background) runs continuously from 15:58:30. It has its own entry (created by itsSubagentStart). - 16:09:19 it spawns a child with the Agent tool (child
ad36795…, ran 16:09:21 to 16:14:32); 16:12:45 another child (afad1ed…, ran 16:12:47 to 16:14:59). - A new entry with an unknown
agent_idand noagent_typeappears at 16:09:43 (+/- 20 s). - 16:14:51
a37ed07…runsBash: lsof -nP -iTCP -sTCP:LISTEN …(present inagent-a37ed07….jsonlas its owntool_use). The hook payload for that call is recorded on the stray entry, not ona37ed07…'s entry. - 16:15:06
a37ed07…runsBash: cd ~/projects/<repo> && …. That payload is recorded ona37ed07…'s own entry again (agent_type: claudepresent). - The stray entry never receives another payload and never receives
SubagentStop.
The other four cases (every subagent named below has its own entry, created by its own SubagentStart, and its own SubagentStop was later matched):
| stray entry created | subagent active at that moment | last payload recorded on the stray entry | which real call that matches |
|---|---|---|---|
| 07:20:45 +/- 1 min | aecdca5… (claude, ran 07:07:34 to 07:48:55, never resumed; its Bash: cd … && sed -n … is at 07:20:44) | Bash: cd ~/projects/<repo> && … | one of aecdca5…'s cd … && … calls between 07:20:44 and 07:21:49; nothing after 07:21:53 (its Read/Edit calls from then on were not recorded on the stray entry) |
| 08:35:45 +/- 1 min | a74c422… (claude, ran 08:31:12 to 09:04:20, never resumed) | Bash: cat > ~/… | no cat > command exists in a74c422…'s transcript at all. Calls of that shape exist later, by other subagents: a799181… (09:36 to 09:37), ab28539… (09:53:05), a37ed07… (16:14:04). So this stray id apparently kept receiving payloads for an hour or more, from a different subagent than the one active when it was created |
| 09:20:45 +/- 1 min | a355335… (claude, ran 09:09:48 to 11:40:45; its Bash: cat > /private/tmp/… is at 09:20:54) | Bash: npx tsc --noEmit … | not uniquely determined: a355335… ran it at 10:06:40, 11:27:45 and 11:39:49, and its children afb3c2d… (09:44:48), ab28539… (09:56 to 10:05) and af5b526… (10:08:45) ran it too. Either way the stray entry kept receiving payloads for at least 45 minutes after creation |
| 13:32:45 +/- 1 min | af2b998… (claude, ran 11:55:44 to 14:44:06; it resumed a completed child with SendMessage at 13:31:52) | SendMessage | af2b998…'s SendMessage at 13:31:52 or 13:38:12 (nobody else called SendMessage in that window); its Bash at 13:38:23 and everything after were not recorded on the stray entry |
| 16:09:43 +/- 20 s | a37ed07… (see above) | Bash: lsof -nP -iTCP … | a37ed07…'s lsof at 16:14:51 (the only lsof call by anyone in that window); its next Bash at 16:15:06 was recorded on its own entry |
I could not find a condition that predicts when the stray id is used. Two of the five began right after the subagent spawned or resumed a child (16:09:19 Agent tool; 13:31:52 SendMessage to a completed child) and stopped receiving payloads around the time that child finished, but another subagent (aadb709…) that spawned children in the same minutes was reported under its own id throughout, and two of the five subagents (aecdca5…, a74c422…) had no child running and were never resumed.
None of the subagent transcripts contain a compaction boundary in those windows, all Agent tool calls involved were single tool calls per assistant message (no parallel batches), and no type: prompt / type: agent hooks are configured (only command hooks).
What Should Happen?
Per the hook input schema, agent_type is "present when the hook fires from within a subagent (alongside agent_id)". Every PreToolUse/PostToolUse fired inside a subagent should carry that subagent's agent_id (the one its SubagentStart carried) and its agent_type. Any agent_id that appears in hook payloads should be bracketed by a SubagentStart before and a SubagentStop after; an id that gets neither leaves every agent_id-keyed consumer with an entry it can never close.
Error Messages/Logs
No error output. The consumer keeps state rather than a raw payload log, so I do not have the raw stray-id payloads; I can add a raw stdin-logging hook and attach them if that helps. The reconciliation above comes from the consumer's entries (creation time, agent_type never set, last tool_name/tool_input) cross-referenced with subagents/agent-*.jsonl, agent-*.meta.json, and the parent session transcript.
Steps to Reproduce
Not reproducible on demand; observed 5 times in one session.
- Register a
PreToolUse/PostToolUse(matcher*),SubagentStart,SubagentStophook that appends its stdin JSON to a file. - Run a long session that spawns background subagents with the Agent tool, lets those subagents spawn their own children, and resumes completed subagents with
SendMessage. - Reconcile: collect every
agent_idseen inPreToolUse/PostToolUse; for each, check whether aSubagentStart(withagent_type) precedes it and aSubagentStopfollows it. - Some ids will have neither. Match their
tool_inputand timestamps against~/.claude/projects/<project>/<session>/subagents/agent-*.jsonlto see which real subagent made those calls.
Claude Model
Main session: claude-fable-5. Subagents: opus (per .meta.json).
Is this a regression?
Unknown. The same hooks have been in place for months and these entries had not accumulated before; I do not have a last known good version.
Last Working Version
Unknown
Claude Code Version
2.1.251
Platform
macOS
Operating System
macOS 26.5.1 (Darwin 25.5.0), Apple Silicon
Terminal/Shell
zsh, third-party terminal emulator (not the desktop app)
Additional Information
- Related: #89555 (
SubagentStopwith freshagent_id, emptyagent_type, after parentStop), #87065 (internal forks fireSubagentStopwith emptyagent_type), #59719, #88995. Those are about stop events with a stray id. Here the stray id shows up onPreToolUse/PostToolUseof a real subagent's own tool calls, and gets no start or stop at all, so it looks like a different producer (or the same producer also being used to execute the parent's tools). - The affected subagents were all
agent_type: claude,spawnDepth: 1, asynchronous background subagents (the parent kept working while they ran), in a session that also usesSendMessageto resume completed subagents and theTaskStop/ListAgentstools.