[BUG] (Async) Subagents stopping early
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Subagents spawned via the Task tool with run_in_background: true can stop executing before completing their work, and the Claude Agent SDK reports them as <status>completed</status> to the parent agent. The parent has no reliable way to distinguish a subagent that finished successfully from one that was terminated prematurely.
I see subagents that:
- Make anywhere from 5-40 tool calls over 2-10 minutes
- Were still actively making tool calls when execution stopped (stop_reason: None on final messages)
- Never reached the final step of their instructions (writing output to a file)
- Were reported to the parent as <status>completed</status>
Essentially, the agent stopped mid-work, and the SDK told the parent it "completed."
This is a recurring pattern, seems to happen in about 14-30% of agent runs. I do not think this is a prompting issue because it always happens after some tool results - the subagent uses a tool, gets the result, and just stops without producing any output.
What Should Happen?
- The SDK should not report completed when a subagent was terminated before it chose to stop. The stop_reason: None on the subagent's final messages indicates the agent did not choose end_turn -- something external ended the session. The task notification should reflect this (e.g., <status>terminated</status> or <status>interrupted</status>).
- The task notification should include a stop_reason or termination_reason field so the parent agent can more easily determine what happened and decide whether to retry.
Error Messages/Logs
The task notification for the prematurely stopped subagent:
<task-notification>
<task-id>aaa583525301d9735</task-id>
<tool-use-id>toolu_011KhgSzp2v9Cd9iANWVUUYf</tool-use-id>
<output-file>/tmp/claude-1000/-home-user/tasks/aaa583525301d9735.output</output-file>
<status>completed</status>
<summary>Agent "Research TERN-501 and TERN-801" completed</summary>
<usage><total_tokens>64000</total_tokens><tool_uses>44</tool_uses><duration_ms>143711</duration_ms></usage>
</task-notification>
Compare to a subagent that actually finished its work (also reports completed, but includes a <result> block):
<task-notification>
<task-id>a9969619664ec9d75</task-id>
<tool-use-id>toolu_01HTpSYExfZarv9benv4oFAE</tool-use-id>
<output-file>/tmp/claude-1000/-home-user/tasks/a9969619664ec9d75.output</output-file>
<status>completed</status>
<summary>Agent "Research ORX142 and ORX489" completed</summary>
<result>
All 61 citations passed verification with zero errors and zero warnings. The research notes file is complete and verified.
The research findings have been written to `/home/user/working/research_notes/orexia_assets.md`.
</result>
</task-notification>
The only observable difference is the presence/absence of <result>. The <status> is identical in both cases.
Subagent's final messages before it stopped (from the subagent transcript research-assistant_aaa583525301d9735.jsonl):
The subagent's last three assistant messages all have stop_reason: None (not end_turn), and the agent was actively issuing tool calls:
[assistant] stop_reason: None
TEXT: "Let me read the S-1 for patent expiry details..."
[assistant] stop_reason: None
TOOL: mcp__local__read_source(source_id: src189)
[assistant] stop_reason: None
TOOL: mcp__local__read_source(source_id: src185)
The agent never chose to stop. It was still working when execution ended.
Steps to Reproduce
- Create a parent agent th1at spawns a subagent via the Task tool with run_in_background: true.
- Give the subagent a prompt that requires many sequential tool calls before producing output. For example: "Search for and read 15 source documents about [topic], then write a comprehensive summary with citations to /home/user/working/research_notes/output.md."
- The subagent will begin actively researching (search, read, search, read...). At some point, execution stops.
- The parent receives a task notification with <status>completed</status> and no <result> block.
- The output file was never created. The parent discovers this only by checking the filesystem.
Note that it's a relatively transient issue, just one that causes a lot of waste.
Claude Model
Opus
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
2.1.104
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Non-interactive/CI environment
Additional Information
_No response_
11 Comments
I agree that this is a transient issue, but one of the constant sources of frustration as it continues to be pervasive. This is often a daily issue for me, and though it typically just takes a nudge to get things moving again, I've had times where every single nudge merely made it take one more step to then halt yet again. In some cases, the ask wasn't even large: a single tool call and then a stall. I have found it to be more frequent when a subagent has a larger amount of context, but I have also seen it when there is virtually no context.
Observed:
run_in_background:truesub-agent stalls at WE-class scaffolding scopeSharing data points that may be relevant to this issue.
Setup: Rory (a CC-based orchestrator agent) dispatches Sonnet sub-agents from scheduled heartbeats to execute multi-file scaffold tasks on a Next.js/Prisma project. Tasks at this scope typically run 28–45 tool uses, touching 8–15 files.
Three background stalls observed:
| Date/Time (PT) | Mode | Tool uses at stall | Final message (truncated) | stop_reason |
|---|---|---|---|---|
| 2026-05-16 19:08 |
run_in_background:true| ~41 | "Now create the actions tests:" |None|| 2026-05-17 05:55 |
run_in_background:true| ~28 | "Now let me create the server actions:" |None|| 2026-05-17 16:13 |
run_in_background:true| ~35 | "Now create the actions tests:" |None|In all three cases: final assistant message was truncated mid-sentence, no
<result>block was produced,stop_reasonwasNone, and the parent session reported the sub-agent as "completed."One foreground success at identical scope:
run_in_backgroundNOT set. Completed cleanly in ~6 min wall-clock, produced a full<result>block, opened PR #17.Conclusion: The stall appears async-specific. Foreground execution at identical scope and tool-use count succeeds where background fails. We are adopting foreground as the default for multi-file scaffolding sub-agents as a workaround pending a fix.
Happy to share additional details if helpful.
The 14-30% silent-completion rate is the most damaging part of this — a 1-in-4 to 1-in-7 false-completion rate is enough to make any orchestrator that branches on
<status>completed</status>unsafe for production use, but low enough that it passes a casual demo. The orchestrator code that handles a real completion vs. a premature stop looks identical to the orchestrator, and the failure presents as "downstream consumer got bad/empty inputs" — which gets attributed to model quality rather than to a missing transport event.A few notes from running an out-of-process orchestrator over a similar
Task(run_in_background:true)fan-out:stop_reason: Noneis the diagnostic primitive but it's not surfaced. The fact that the final message has no model-emitted stop reason is the clearest signal "this was terminated, not finished." Today the SDK collapses that intostatus: completedat the orchestrator boundary, losing the distinction. Surfacingstop_reason: null | "end_turn" | "max_tokens" | "tool_use" | ...directly to the orchestrator (or, even simpler, a booleanmodel_chose_to_stop: bool) would let consumers reject the premature-stop case without needing access to the underlying transcript.completedstatus.statusfield can be wrong but the artifact's existence on disk can't.completedbut its activity counter never reached the expected tail (e.g., the finalWritetool call), treat the return as suspect. Heuristic, but catches the modal failure shape.last_message.stop_reasonto the orchestrator would let the consumer make the call. Acompletedstatus withstop_reason: nullis the structural signal "I stopped but the model didn't choose to" — that's the actionable bit, and it's information the SDK already has.task_notification(completed)firing before the subagent'sResultMessage— different timing, but the same underlying "completion is being signaled before the subagent's terminal state is durable." Fixing either one in isolation closes only half the surface; fixing the underlying "completion is multi-phase and the orchestrator only sees the first phase" pattern closes both.The cleanest end-state is: orchestrator subscribes to one event that's emitted only after the subagent's terminal state is durable on the transport stream. Everything else is a hint. Subscribing.
+1 to the repro steps above - we consistently see this with subagents that need to do many tool calls (20+). However, we also see it for foreground agents, not just ones run in the background. Agents will receive tool results from some tool call, and then do not take another turn. The subagent transcript ends with the tool results. Most recent stop_reason is tool_use (normal), before it got the tool results. At the least, we'd also like a more clear reason given for stopping!
This looks like the kind of bug that shows up when subagent execution is treated as “just spawn more workers” without a strong completion contract.
The first thing I’d check is whether the parent/coordinator has a clear definition of:
A practical pattern that helps a lot is:
In multi-agent systems, the failure is often less about parallelism itself and more about ambiguous lifecycle boundaries between planner and executor.
If this is already on your radar, I’d also look at whether the subagent payload is carrying more bootstrap/context than it needs. Oversized inherited context tends to make async worker behavior less predictable and harder to debug.
Corroborating, plus an extra axis: this splits almost entirely on model tier — and forced structured output is immune.
Confirming this reproduces on foreground (synchronous)
Tasksubagents too, not justrun_in_background: true(matching @astreet's note above). I ran a controlled sweep and the loss rate tracks the subagent's model tier far more than anything else.Setup: Claude Code, subagents spawned via the Task/Agent tool with an explicit per-call
model. Each probe is handed a unique marker token and instructed to make that token its entire final message; PASS = the marker actually arrives back at the parent. (Disclosure: my session also has a hook-injecting plugin active — see the last point; it may be a contributing trigger, so treat the absolute rates as environment-flavored, but the cross-model contrast is the signal.)Sweep A — 96 probes (8 replicates × 3 models × 4 modes):
| model | no-tool (short) | no-tool (long) | one web search | forced structured output |
|---|---|---|---|---|
| opus | 8/8 | 8/8 | 8/8 | 8/8 |
| sonnet | 1/8 | 0/8 | 6/8 | 8/8 |
| haiku | 0/8 | 0/8 | 2/8 | 8/8 |
**Sweep B — 16 probes, a real research subagent (do web lookups, report a version + cite the URL — i.e. genuine free-form prose, the modal use case): opus 8/8 returned the cited report; sonnet 0/8** (all 8 sonnet returns were degenerate).
Across both sweeps: opus 48/48 surfaced, non-opus free-form lost the large majority of the time.
Three observations that may help localize it:
"(Standing by.)","(Awaiting new input)","Task finished.","Subagent task concluded.","Acknowledged.", plus several non-English idle phrases like"확인했습니다."("understood") and"무엇을 도와드릴까요?"("how can I help you?"). These read exactly like a fresh-turn / idle assistant message. That's consistent with the "completion is multi-phase and the orchestrator only sees the wrong phase" theory in this thread: the real answer turn (the one that should carryend_turn) is being clobbered by a later degenerate turn, and that later turn is what gets surfaced.StructuredOutputtool call) is 100% immune across all three models (24/24, incl. sonnet & haiku). But this is specifically about the tool-call channel, not** about asking for "structured-looking" text: as a negative control, asking sonnet to make its entire final message a JSON object / fenced ``json block (no tool call, just JSON-shaped prose) still fails — **0/4**, returning the same idle fragments ("대기 중입니다.","무엇을 도와드릴까요?"`). So the payload surviving has nothing to do with it being JSON; it survives only when it rides in tool-call args instead of the terminal assistant message. Strong hint the defect is in terminal free-form text surfacing/selection, not in the agent loop or output formatting.A concrete trigger to check: a few of the degenerate captures were literally the subagent answering a hook injection —
"(duplicate hook — ignoring.)","(duplicate hook — no action)". In my setup a plugin injects system-reminders into subagents, and it looks like a hook-driven turn fired after the model had already produced its answer can become the "final message." If the runtime can append/force any turn after the model'send_turn, non-opus models tend to answer it with idle filler — and that filler is what surfaces to the parent. Might be worth checking whether SubagentStop / continuation injection can produce a terminal turn after the model has already chosen to stop, and whether the "final message" selection should prefer the lastend_turnmessage over the literal last message.---
Minimal repro. Spawn the same prompt twice via the Task tool, once with
model: "opus"and once withmodel: "sonnet"(or"haiku"), and compare what comes back to the parent. The exact probe prompts I used (replace<MARKER>with any unique token, e.g.MARK-7f3a):``
``Do NOT use any tools. Your ENTIRE final message must be exactly this single line and nothing else:
<MARKER>
``
``Do NOT use any tools. First write exactly three sentences about the number seven. Then, on its own final line, output exactly this and nothing after it:
<MARKER>
``
``Do EXACTLY ONE web search for "current stable version of curl", then STOP searching (no more tool calls). Your ENTIRE final message must be exactly this single line and nothing else:
<MARKER>
{"marker": "<MARKER>"}via a tool/JSON-schema-constrained output instead of free text:``
`Echo the marker value verbatim into your structured output. The marker is: <MARKER>
{"type":"object","required":["marker"],"properties":{"marker":{"type":"string"}}}`.with schema
``
``No tools. Your ENTIRE final message must be exactly this JSON and nothing else:
{"answer": "<MARKER>"}
``
``Search the web for the <e.g. "Node.js current LTS"> version number. Report: the exact version, the source URL, and a one-sentence note. Cite the URL.
PASS = the marker / cited report actually reaches the parent; FAIL = parent receives an idle/wrap-up fragment instead. I ran 8 replicates per cell; happy to share the full harness or raw per-run output if useful.
---
Follow-up / correction to my comment above — root cause found; this is plugin-induced, not a vanilla Claude Code bug.
My comment above mis-scoped this. I can now toggle it on/off deterministically: it's caused by my plugin (oh-my-claudecode), not vanilla Claude Code. Leaving both comments up so the progression is visible; two harness-side robustness points remain Anthropic-relevant (C1/C2 below).
1. Vanilla vs plugin (same probe, spawned via Task tool at each tier)
Probe:
Do NOT use any tools. Your ENTIRE final message must be exactly this single line and nothing else: <MARKER>Lines [10]–[20] are all assistant messages chained by
parentUuidwith no user turn between them — the runtime kept re-invoking the subagent after it had already emittedend_turn.2. Root cause, confirmed by toggling one hook output
My plugin's SubagentStop hook returns
{continue:true, hookSpecificOutput:{additionalContext:"Agent X completed"}}unconditionally on every subagent completion. I patched only that one hook to return{continue:true, suppressOutput:true}instead (same plugin-on session, nothing else changed):0% → 100% by toggling that single line, both directions. So the
additionalContextreturned from SubagentStop is being fed back into the just-finished subagent and re-invoking it; each re-invocation ends → SubagentStop fires again → re-injects → loops.Why model-dependent: re-invoked with no real task, opus re-emits its prior answer (stable) so the surfaced last turn stays correct; sonnet/haiku drift to a generic greeting, which is what gets surfaced.
Two harness-side points that look Anthropic-relevant regardless of the plugin
additionalContext(withcontinue:true) can re-invoke, and here loop, a subagent past theend_turnit already chose. Should a Stop/SubagentStopadditionalContextever re-drive a subagent that already emittedend_turn?end_turnturn. Once extra turns exist, the real answer is discarded even though it's right there in the transcript.Fixing it in the plugin; the model-tier data in the comment above still stands as a description of which tiers degrade under this re-invocation.
Corroborating evidence from CC 2.1.170, macOS Darwin 25.5.0 (arm64), 2026-06-10/11.
Observed: 9 out of 11 background subagents (all
claude-sonnet-4-5) died mid-task within a single ~50-minute UTC window (13:50–14:39 UTC = 23:50–00:39 AEST). Same pattern as reported here:usermessage carrying atool_resultstop_reasoncounts show zeroend_turnacross all assistant messages in every dead agentmeta.jsonfor all dead agents:status=null, exit_code=null, result=""stop_reason distribution across 11 analysed transcripts:
| Agent ID (truncated) | Lines | Last entry type |
end_turn|tool_use|None| Notes ||---|---|---|---|---|---|---|
| a013e29d | 43 |
user(tool_result) | 0 | 8 | 16 | dead || a01945480 | 85 |
user(tool_result) | 0 | 9 | 42 | dead || a39b3b81 | 122 |
user(tool_result) | 0 | 17 | 53 | dead || a43aa5bc | 75 |
user(tool_result) | 0 | 12 | 32 | dead || a959962a | 59 |
user(tool_result) | 0 | 6 | 26 | dead || ab34e510 | 48 |
user(tool_result) | 0 | 9 | 18 | dead || abdd4083 | 93 |
user(tool_result) | 0 | 16 | 41 | dead || ac99c44e | 31 |
user(tool_result) | 0 | 5 | 12 | dead || af8aefab | 30 |
user(tool_result) | 0 | 6 | 11 | dead || a1d7c438 | 122 |
assistant| 1 | 11 | 55 | survived (reached end_turn) || a2822a19 | 98 |
assistant| 0 | 23 | 38 | completed normally (result written) |Sample transcript tail (representative dead agent):
What we ruled out:
Distinguishing characteristic: All 9 deaths are tightly time-correlated (within a ~50-minute UTC window). Agents dispatched outside this window behaved normally in the same session with identical prompts and tooling. This time-correlation pattern suggests a transient backend condition (capacity, infrastructure event) rather than a purely harness-side race condition — though the false-completed reporting is the same harness-level defect described here.
CC version: 2.1.170 | Model: claude-sonnet-4-5 | Platform: macOS arm64 | API: Anthropic direct
Closing for now — inactive for too long. Please open a new issue if this is still relevant.
Adding independent reproduction data + a quantitative baseline, since this issue matches what I'm seeing very closely (including the 14-30% rate estimate).
Environment: macOS (darwin), Claude Code native install, versions 2.1.241/2.1.242 observed (issue predates and postdates a binary update to 2.1.246 within the same day, so it's not tied to one specific patch version).
Historical baseline (retrospective log analysis): Scanned local subagent transcripts (
~/.claude/projects/**/subagents/agent-*.jsonl) across multiple long-running projects for the period 2026-08-01 to 2026-08-19. Of 191 subagent invocations that used at least one tool, 171 continued normally past the first tool call (received atool_resultand kept going) and 20 stopped immediately after emittingtool_use, withstop_reason: tool_useand notool_resultever appended to the transcript — a ~10.5% silent-stop rate, consistent with the 14-30% figure already reported in this thread.Today's spike (2026-08-26): Ran a set of controlled reproductions dispatching fresh (non-fork)
Agent-tool subagents viarun_in_background-style async launch:ToolSearch, a plainReadof a single small file, and a trivialBashcommandecho hello, across both a generic subagent type and a project-specific subagent type with pre-declared — not dynamically searched — tool access): 9/9 stopped immediately after the tool_use, never receiving the tool_result, and were reported by the harness asstatus: completedwith no error.So on this occasion the failure rate for tool-using dispatches was effectively 100%, well above the historical ~10.5% baseline — worth noting since it suggests the rate isn't stable and can spike under conditions I haven't isolated yet (tool type, tool latency, and prompt size were all ruled out as factors in the controlled tests above — every one of them failed at the very first tool call regardless).
Why this is worth flagging on this specific issue: the failure signature matches exactly —
stop_reason: tool_useon the last message, zerotool_resultentries afterward,status: completedreported to the parent/orchestrator. Agreeing with the comment above that this makes any orchestration pattern that trustsstatus: completedunsafe without independent verification of the subagent's actual side effects.Happy to share the (locally-run, no external data) analysis script if useful.
This issue was incorrectly closed, despite having reproduction steps and Claude version. This behavior of the bot is tracked in https://github.com/anthropics/claude-code/issues/87647.