TeammateIdle fires for worker-fork idles with the parent teammate's transcript_path and no agent identification — feedback loops become unsatisfiable for the fork
Summary
When a worker fork (Agent tool, subagent_type: "fork") spawned by a teammate goes idle, the TeammateIdle hook event fires with the parent teammate's transcript_path and a payload that is byte-identical to the one fired for the teammate's own idle. There is no agent_id, agent_type, or any other field identifying the idling process — so a hook cannot tell a fork's idle from the teammate's, and cannot resolve the fork's actual transcript.
The exit-2 stderr feedback, however, is delivered to the fork's loop (and recorded in the fork's transcript under <session>/subagents/agent-<id>.jsonl). Any hook whose stand-down predicate reads the transcript it was handed (e.g. "did the teammate reply since the last lead message?") becomes unsatisfiable for the fork: everything the fork does lands in its own transcript, which the hook never sees.
Environment
- claude 2.1.219 (native binary), Linux
- Agent team: lead session + tmux teammate; teammate spawns a worker fork
Observed
Captured TeammateIdle payloads (via a hook that dumps stdin) for both the teammate's own idles and its fork's idles — all five events, byte-identical:
{
"session_id": "95a215f6-...",
"transcript_path": ".../projects/<project>/95a215f6-....jsonl",
"cwd": "...",
"permission_mode": "bypassPermissions",
"hook_event_name": "TeammateIdle",
"teammate_name": "nag-probe",
"team_name": "session-cddaed71"
}
Consequences observed with a reply-nudging TeammateIdle hook (exit 2 unless the transcript shows a reply/ack to the lead's last message):
- A fork answered its task (
4), got the exit-2 feedback, emitted the hook's documented bypass sentinel, got nagged again (the sentinel is in the fork's transcript, the hook reads the parent's), and looped — one fork looped 9+ times. - The loop corrupts the fork's return value: the fork eventually returned the bypass sentinel text instead of its actual answer.
- The fork cannot even be rescued by "only nag once" logic keyed on the hook's own feedback marker, because that marker is written to the fork's transcript while the hook analyzes the parent's.
Expected
Either of:
- Include agent identification in the payload —
SubagentStopalready gets exactly this (agent_id,agent_transcript_path,agent_type);TeammateIdle's hook input is built from the base input only, without threading the tool-use context through, so the fields come out undefined. Symmetry withSubagentStopwould let hooks resolve the idling process's own transcript. - Or don't fire
TeammateIdlefor worker-fork idles — the event semantically describes the teammate, and the teammate isn't idle (it's usually awaiting the fork).
Notes for anyone hitting this
- Detecting fork idles by comparing transcript mtimes (
subagents/agent-*.jsonlnewer than the main JSONL ⇒ fork idle) does not work: transcript writes are buffered, and the fork's final entries often flush only after the hook has run (same flush-timing family as #25121). - Reliable hook-side workaround: track the fork spawn at
PostToolUse(the Agent response hasisAsync: true/status: "async_launched"— notetool_inputcarries norun_in_backgroundfor fork spawns) and stand down while the session has an in-flight agent task. A fork's terminal task-notification only reaches the parent transcript after its idle completes, so this reliably covers the fork's own idles too. Implemented in Butanium/claude-code-hooks@0a826e7.
---
🤖 Filed by Claude (Fable 5) via Claude Code, investigating on behalf of @Butanium — repro, payload captures, and the workaround are from a live debugging session. (Co)-Authored-By Claude Fable 5 <noreply@anthropic.com>
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗