Background subagents intermittently stall with no final text, harness still reports status:completed (fresh subagent types only, not fork)
Description
Background subagents launched via the Agent tool with a fresh subagent type (e.g. general-purpose) intermittently stall before producing any final text, while the outer harness still reports status: completed. The subagent's raw transcript (~/.claude/projects/<project>/<sessionId>/subagents/agent-<id>.jsonl) simply stops mid-conversation — sometimes right after the very first tool_use with no tool_result ever appended, sometimes after several tool calls have already received real tool_results but before any final assistant text is produced.
In the same session, subagent_type: "fork" (which inherits the parent's conversation context instead of starting cold) completed reliably across every repeat.
Steps to reproduce
- In an existing Claude Code session with some conversation history, call the
Agenttool withsubagent_type: "general-purpose"and a small multi-step task (e.g. rungit log, read 2 files, then summarize in a final message). - Wait for the
<task-notification>— it reportsstatus: completed. - Inspect the subagent's raw transcript file directly (not the
.outputsymlink's rendered summary, the actualagent-<id>.jsonl). In the failing cases, the last event is either:
- An
assistantmessage whose only content block istool_use,stop_reason: "tool_use", with no subsequenttool_result— the harness never delivered/recorded the result of that call, or - A
tool_resultfor the last invoked tool, but no subsequent assistant message with real final text — the agent never got to (or never produced) a wrap-up response.
- Repeat the same style of task with
subagent_type: "fork"instead — it consistently completes with alltool_results delivered and a real final text answer.
Environment
- Claude Code CLI,
version: 2.1.221(from subagent transcriptversionfield) - macOS (Darwin 25.5.0)
- Model:
claude-sonnet-5 - Reproduced under multiple session permission modes:
auto,plan, and with--dangerously-skip-permissions, as well as this project's own separate "workflow mode" concept (lean) — the failure is identical across all of them, so it does not look like a permission/classifier issue.
Evidence gathered so far
- 2/2 fresh-agent (
general-purpose) background calls stalled in one test session: - Trivial 1-tool-call task: transcript ends right after the single
Bashtool_use, notool_result, no text. Notification reportedtool_uses: 1,duration_ms: 2880. - Multi-step task (git log/ls, read 2 files, summarize): 2
Readcalls got realtool_results back, but theBashcall'stool_resultnever appeared, and the transcript ends with no final assistant text at all. Notification reportedtool_uses: 3,duration_ms: 3793. - 3/3 same-session
subagent_type: "fork"calls, given an equivalent multi-step task, completed cleanly — alltool_results delivered plus a real final-text answer each time (duration_ms: 10507,11089,12938— longer than the failed runs, so it isn't simply "faster tasks survive"). - The one structural difference we could identify between the two subagent types: a fresh subagent (
general-purpose) begins its transcript with a large (~35k-token) attachment dump (a permission diff + the full skill listing for the project) before its own first turn;forkinherits the parent's existing context and skips that cold-start dump entirely. - Ruled out project-local interference: this project registers two hook scripts (on
PreToolUse/PostToolUse/SubagentStart/SubagentStop/Stopetc.). Verified one is a no-op in this environment (env vars it needs are unset) and the other only fires on the main session'sStopevent, never onSubagentStop, so it cannot be blocking a subagent's tool_result delivery in real time. Neither hook correlates with which runs failed vs succeeded.
Working hypothesis (unconfirmed)
The stall may be tied to a fresh subagent's cold-start attachment write (the large initial permission-diff + skill-listing payload) — possibly a race between flushing that large initial payload to the subagent's transcript and delivering the next real event (a tool_result or the final assistant message), which can silently drop that next event without corrupting anything already written. This is a hypothesis from a small sample (2 failures / 3 successes) in one session, not a root cause.
Impact
Silent data loss with a misleading status: completed is worse than an explicit failure: callers of the Agent tool have no signal that the subagent's result is missing, and — per this project's own experience — the working guidance became "treat status: completed with a suspiciously low tool_uses and short duration_ms as absent, not empty," and "prefer fork over a fresh subagent type for background delegation," neither of which should be necessary.
Showing cached comments. Read the full discussion on GitHub ↗
4 Comments
Additional evidence: Claude Code has a documented transcript-flush race around turn completion
While investigating whether the terminal emulator (Warp) could be involved, I found that Anthropic's own
claude-code-warpplugin (installed via the plugin marketplace) works around a related, already-known race condition — which may share a root cause with this issue.In
scripts/on-stop.shof that plugin:So it's already known and compensated for, in at least one first-party integration, that the
Stophook can fire before the main session's transcript file has finished being written to disk — the plugin author added a fixedsleep 0.3before reading the transcript to work around exactly that.This is suggestive for the bug reported here: if an equivalent race exists on the internal path that marks a background subagent as
status: completed(i.e. that signal can fire before the subagent's own transcript file has received its lasttool_resultor final assistant-text event), that would produce exactly the failure mode described above — no visible error,status: completed, but the last event silently missing.One data point against a pure delay/lag explanation: I re-checked the two stalled transcripts from my original repro several hours after they occurred (same session) — file size, line count and mtime are all unchanged since the moment they stalled. So if this is the same class of race, it isn't a race that resolves itself given more time; whatever event is missing from the subagent's transcript appears to be lost permanently, not just delayed. That may still be consistent with a flush race (e.g. the writer never gets scheduled again, rather than being merely slow), but it rules out "just poll longer" as a workaround.
Environment note: this was observed on Claude Code 2.1.221, macOS, running inside Warp terminal — but the mechanism above (the
Stop-hook/transcript-write race) is a Claude Code behavior independent of Warp; Warp's plugin is simply the first-party integration that happened to document and work around it.Additional reproduction (2026-08-05) — 3 independent occurrences, distinct agentIds
Three separate stalls observed, same pattern, different
agentIds each time — not the same instance repeated:agentId=af9464d4aa41e89b8(attributionAgent: "Explore"): 8tool_useentries, 0tool_resultentries in the transcript. Ends mid-flight on a plain shell command (stop_reason: "tool_use"), no final text.agentId=a7a831cfdbe2a14db(attributionAgent: "Explore"): 4tool_use, 0tool_result. Same pattern. Both from the same coordinating session, timestamps ~07:31 UTC.agentId=ae450b0b04150273d(agentType: "Explore"): separate live reproduction viaclaude --debug-file, ~07:59 UTC, same day.For (1) and (2), the coordinating session actively prompted the stalled subagent for its final report mid-flight ("Please repost your complete final report text verbatim... I need it as plain text") and got no response — ruling out "nobody checked, so it looked idle."
Live reproduction (3) via:
grep -n "agent_completion" /tmp/cc-debug.log:(
[Stall]is a general logging namespace also used for unrelated healthy tool-dispatch events — not claiming it's a dedicated failure detector, just noting the field values:finalStopReason=tool_use+lastToolResultSeen=nonewhileexitPath=completed.)No permission-denial, EACCES, sandbox, or hook-related output appears anywhere in the debug log around any of the three occurrences — ruling those out as a cause on this environment (macOS, Claude Code 2.1.222).
Correction to my 2026-08-05 comment above — the repro was confounded by a repo-local hook, not a pure harness bug
Ran a controlled A/B test on the exact same reproduction: same prompt, same
subagent_type="Explore", same environment — the only variable changed was whether our repo's ownPreToolUsehook (aBash-matcher hook that can returnpermissionDecision: "defer") was registered in.claude/settings.json.turns=1,finalStopReason=tool_use,lastToolResultSeen=none. The debug log shows the hook returningdefertwice for plain read-onlyBashcalls (e.g.grep) immediately before the stall.turns=4,finalStopReason=end_turn,lastToolResultSeen=<matching tool_use id>, and produced a real, correct final report.So this looks like an interaction between our project's own hook (which deliberately returns
defer/"abstain" onPreToolUse:Bash) and backgroundExploresubagents, not a harness defect in isolation. I don't have proof of the exact mechanism (whether it's a turn/time budget exhausted by hook round-trip latency, ordefernever resolving for a non-interactive background subagent, or something else) — but the A/B correlation is solid enough that I shouldn't have framed the original report as a pure Claude Code bug.Apologies for the noise — leaving the original repro data up since the raw
agent_completion/lastToolResultSeen=nonesignature may still be useful if others see the same pattern without a PreToolUse hook involved, which would point back to the harness. If it turns out this only reproduces with adefer-returningPreToolUsehook in the loop, that's more actionable as: "background subagents don't have a resolution path for a deferred/ask permission decision," which is still worth a decision either way (fail loud, or auto-resolve consistently) rather than silently completing with no result.Final update (2026-08-05) — root cause narrowed, local mitigation confirmed, no harness change needed on our end
Following up on the correction above: root-caused this fully now with a controlled A/B test isolating the actual variable.
Root cause, precisely stated: it is not
deferspecifically, and not our repo's mode logic. It's that anyPreToolUsehook emitting stdout for a tool call made by a background subagent (payload carriesagent_id/agent_type) leaves that tool call without atool_resultand the run gets reportedcompletedregardless — even when the hook's own decision is a documented no-op (defer, "apply the ordinary permission flow"). Confirmed via a real A/B: same prompt, samesubagent_type="Explore", same environment — with ourPreToolUse:Bashhook registered, the subagent stalled (turns=1,finalStopReason=tool_use,lastToolResultSeen=none); with the hook entirely absent, it completed normally (turns=4,finalStopReason=end_turn,lastToolResultSeenmatchinglastToolUseId).Local mitigation, tested and applied: our hook now detects
agent_idin the payload and returns no stdout at all for those calls, while logging the bypass to a local ledger (not silent). Verified this is safe on our end specifically: our hook's underlying decision function only ever returnsallowordefer— it can neverdeny— and the actual deny-list enforcement lives independently insettings.json'spermissions.deny, evaluated by the host regardless of hook output. Confirmed in practice, not just by reading the code: with the bypass active, a background subagent's attempt to run a command on our deny list was still blocked at the permission layer before executing, exactly as with the hook fully engaged.Why I don't think this needs a harness fix on Anthropic's side, but flagging anyway: a
PreToolUsehook that returns the documented "no-op, apply ordinary flow" decision (defer) breaking a background subagent's own tool dispatch — while the identical hook is harmless for a foreground/interactive session — is still a surprising asymmetry. If anyone else hits this, the fastest diagnostic is: does removing yourPreToolUsehooks (or making them exit silently for calls carryingagent_id) make the stall go away. If yes, the underlying question worth an answer either way is whether background subagent tool dispatch is expected to tolerate any hook output at all, or whether that's a known limitation.