TeammateIdle fires for worker-fork idles with the parent teammate's transcript_path and no agent identification — feedback loops become unsatisfiable for the fork

Status Open
Reported on v2.1.219
Maintainer reply None cached
Activity 1 comment · opened Jul 30, 2026

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:

  1. Include agent identification in the payloadSubagentStop already 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 with SubagentStop would let hooks resolve the idling process's own transcript.
  2. Or don't fire TeammateIdle for 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-*.jsonl newer 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 has isAsync: true / status: "async_launched" — note tool_input carries no run_in_background for 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>

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗