Background subagents intermittently stall with no final text, harness still reports status:completed (fresh subagent types only, not fork)

Status Open
Reported on v2.1.221
Maintainer reply None cached
Activity 8 comments · opened Aug 4, 2026

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

  1. In an existing Claude Code session with some conversation history, call the Agent tool with subagent_type: "general-purpose" and a small multi-step task (e.g. run git log, read 2 files, then summarize in a final message).
  2. Wait for the <task-notification> — it reports status: completed.
  3. Inspect the subagent's raw transcript file directly (not the .output symlink's rendered summary, the actual agent-<id>.jsonl). In the failing cases, the last event is either:
  • An assistant message whose only content block is tool_use, stop_reason: "tool_use", with no subsequent tool_result — the harness never delivered/recorded the result of that call, or
  • A tool_result for 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.
  1. Repeat the same style of task with subagent_type: "fork" instead — it consistently completes with all tool_results delivered and a real final text answer.

Environment

  • Claude Code CLI, version: 2.1.221 (from subagent transcript version field)
  • 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 Bash tool_use, no tool_result, no text. Notification reported tool_uses: 1, duration_ms: 2880.
  • Multi-step task (git log/ls, read 2 files, summarize): 2 Read calls got real tool_results back, but the Bash call's tool_result never appeared, and the transcript ends with no final assistant text at all. Notification reported tool_uses: 3, duration_ms: 3793.
  • 3/3 same-session subagent_type: "fork" calls, given an equivalent multi-step task, completed cleanly — all tool_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; fork inherits 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/Stop etc.). Verified one is a no-op in this environment (env vars it needs are unset) and the other only fires on the main session's Stop event, never on SubagentStop, 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.

View original on GitHub ↗

4 Comments

Akashae98 · 26 days ago

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-warp plugin (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.sh of that plugin:

# Extract the last user prompt and assistant response from the transcript.
# Small delay to allow Claude Code to flush the current turn to the transcript file.
# The Stop hook fires before the transcript is fully written.
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)
sleep 0.3

So it's already known and compensated for, in at least one first-party integration, that the Stop hook can fire before the main session's transcript file has finished being written to disk — the plugin author added a fixed sleep 0.3 before 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 last tool_result or 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.

Akashae98 · 25 days ago

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:

  1. agentId=af9464d4aa41e89b8 (attributionAgent: "Explore"): 8 tool_use entries, 0 tool_result entries in the transcript. Ends mid-flight on a plain shell command (stop_reason: "tool_use"), no final text.
  2. agentId=a7a831cfdbe2a14db (attributionAgent: "Explore"): 4 tool_use, 0 tool_result. Same pattern. Both from the same coordinating session, timestamps ~07:31 UTC.
  3. agentId=ae450b0b04150273d (agentType: "Explore"): separate live reproduction via claude --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:

claude --debug-file /tmp/cc-debug.log -p "Use the Agent tool with subagent_type=\"Explore\" to search <repo> read-only for <term>, then report a short structured summary."

grep -n "agent_completion" /tmp/cc-debug.log:

[INFO] [Stall] agent_completion agentId=ae450b0b04150273d agentType=Explore exitPath=completed durationMs=4525 turns=1 finalStopReason=tool_use lastChunkAgeMs=0 lastToolUseId=toolu_01HCGTSuRhT5fJh3mmHhjJgi lastToolResultSeen=none

([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=none while exitPath=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).

Akashae98 · 25 days ago

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 own PreToolUse hook (a Bash-matcher hook that can return permissionDecision: "defer") was registered in .claude/settings.json.

  • With the hook present (as in my earlier comment): the Explore subagent stalled — turns=1, finalStopReason=tool_use, lastToolResultSeen=none. The debug log shows the hook returning defer twice for plain read-only Bash calls (e.g. grep) immediately before the stall.
  • With the hook removed (temporarily, for this test only): the identical subagent completed normally — 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" on PreToolUse:Bash) and background Explore subagents, 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, or defer never 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=none signature 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 a defer-returning PreToolUse hook 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.

Akashae98 · 25 days ago

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 defer specifically, and not our repo's mode logic. It's that any PreToolUse hook emitting stdout for a tool call made by a background subagent (payload carries agent_id/agent_type) leaves that tool call without a tool_result and the run gets reported completed regardless — 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, same subagent_type="Explore", same environment — with our PreToolUse:Bash hook registered, the subagent stalled (turns=1, finalStopReason=tool_use, lastToolResultSeen=none); with the hook entirely absent, it completed normally (turns=4, finalStopReason=end_turn, lastToolResultSeen matching lastToolUseId).

Local mitigation, tested and applied: our hook now detects agent_id in 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 returns allow or defer — it can never deny — and the actual deny-list enforcement lives independently in settings.json's permissions.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 PreToolUse hook 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 your PreToolUse hooks (or making them exit silently for calls carrying agent_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.

Showing cached comments. Read the full discussion on GitHub ↗