Nested background agents recursively spawn sub-agents, loop on filler no-ops for 6.5+ hours, and become unreachable/unstoppable once the spawning agent's session ends

Status Fixed / completed
Maintainer reply None cached
Activity 12 comments · opened Jul 3, 2026 · closed Aug 17, 2026

Summary

A background research agent (spawned via the Agent tool with run_in_background) recursively spawned its own further background sub-agents rather than doing the work inline. At least one of those sub-agents got stuck in a loop of filler/no-op tool calls instead of quiescing while waiting for its children's completion notifications, and has now been "running" for 6.5+ hours. Once the spawning sub-agent's own session ended, its children became unreachable from the top-level conversation — TaskStop/TaskOutput return No task found with ID for them — yet they remain visibly "running" in the task panel with no way to stop them.

Steps to reproduce

  1. From a top-level session, use the Agent tool to spawn a background research agent whose prompt covers several independent sub-topics (in our case, a multi-part codebase research request).
  2. That agent's transcript shows it spawning its own set of further background research agents ("Ran 4 agents") rather than answering the sub-topics inline via its own tool calls.
  3. One of those second-level agents then enters a repeating loop of tool calls that do nothing productive while waiting for its own children: "Checked current time," "idled," a widget-context read, a Monitor call that times out, repeated explicit "No-op while waiting for background agents," etc. — instead of just returning control and waiting for the async completion notification.
  4. This loop has continued for 6.5+ hours per the task panel, across (at least) 4 sibling tasks from the same fan-out.
  5. From the top-level conversation, calling TaskStop/TaskOutput on the agent ID visible in that transcript returns No task found with ID: <id> — the top-level session has no handle on a task spawned by an intermediate agent, especially after that intermediate agent's own session has ended.

Expected behavior

  • An agent waiting on its own spawned sub-agents should idle/quiesce and wait for the completion notification, not repeatedly re-invoke itself to perform filler tool calls.
  • Tasks should not become permanently orphaned and unstoppable just because the agent that spawned them is no longer active — the top-level session (or some management surface) should retain the ability to inspect/stop all descendant tasks in the tree, or descendants should be cleaned up when their parent's session ends.
  • (Related, likely a prompting/guardrail issue rather than a hard bug: agents instructed to research a topic inline are instead recursively spawning further background agents, compounding the above.)

Environment

  • Claude Code desktop app for Windows, version 1.18286.0 (259c3f), built 2026-07-02T07:11:03.000Z
  • Windows 11, version 25H2, build 26200.8655
  • Observed via the Agent tool with run_in_background: true triggering nested Agent calls from within a spawned agent

View original on GitHub ↗

10 Comments

yurukusa · 1 month ago

Filing a workaround here since this is 0-comment and the runaway is expensive. I can't reproduce the desktop-app task-panel behavior (I'm on the CLI), so I'll separate what I verified locally from what's inference.
Three failure layers, as I read it:

  1. An intermediate background agent spawns further background agents instead of doing the sub-topics inline (the "Ran 4 agents" fan-out).
  2. A grandchild, while waiting on its own children, re-invokes itself with filler no-ops ("checked current time", "Monitor timed out", "no-op while waiting") instead of quiescing — so it burns tokens indefinitely.
  3. Once the intermediate agent's session ends, the top-level session has no handle on its descendants: TaskStop/TaskOutput return No task found, but the panel still shows them "running".

Layer (1) is the one you can gate deterministically from the client. The recursion only compounds because a sub-agent is allowed to spawn background agents at all.
Deterministic guardrail (verified locally): a PreToolUse hook on Agent/Task that blocks run_in_background:true when the current run is already inside a sub-agent. Claude Code exposes that via the CLAUDE_CODE_CHILD_SESSION env var — it's set inside spawned agent sessions (I confirmed it's inherited into sub-agents while looking at the transcript-inheritance behavior in #73848). I have not confirmed the desktop app sets it identically, so verify on your build first with a trivial printenv hook.

set -euo pipefail
INPUT="$(cat)"
TOOL="$(printf '%s\n' "$INPUT" | jq -r '.tool_name // empty')"
case "$TOOL" in Agent|Task) ;; *) exit 0 ;; esac
BG="$(printf '%s\n' "$INPUT" | jq -r '.tool_input.run_in_background // false')"
[[ "$BG" == "true" ]] || exit 0
if [[ -n "${CLAUDE_CODE_CHILD_SESSION:-}" ]]; then
  echo "Blocked: a sub-agent tried to spawn a background agent (issue #73829: nested background agents orphan and loop). Do the work inline, or spawn background agents only from the top-level session." >&2
  exit 2
fi
exit 0

Wire it under hooks.PreToolUse with matcher Agent|Task. I ran it against three inputs: (a) run_in_background:true with CLAUDE_CODE_CHILD_SESSION set → exit 2 (blocked); (b) same but variable unset (top-level) → exit 0 (allowed); (c) a foreground agent inside a sub-agent → exit 0. So it only bites the nesting case and leaves normal top-level fan-out and foreground agents alone.
Pair it with a CLAUDE.md line: "Sub-agents must not spawn further background agents; research sub-topics inline."
**What this does not fix:** the already-orphaned tasks. Since they're app-managed tasks (not OS processes you can kill) and the spawning session is gone, there's no client-side handle — restarting the app is likely the only way to clear the panel.
The real fixes are provider-side: descendants should stay inspectable/stoppable from the root (or be cleaned up when their parent's session ends), and an agent awaiting children should quiesce on the completion notification rather than emit filler tool calls (layer 2).

yurukusa · 1 month ago

The root cause has three distinct layers, and each needs a different mitigation:

  1. Recursive background spawning — a background agent spawning further background agents instead of doing the sub-topics inline.
  2. No-op busy-loop — the intermediate agent re-invoking itself with filler tool calls ("checked current time", "Monitor timed out", "no-op while waiting for background agents") instead of quiescing on the async completion notification.
  3. Orphaned, unstoppable tasks — once the spawning agent's session ends, the top-level session holds no handle, so TaskStop/TaskOutput return No task found with ID.

Layers 2 and 3 are genuinely provider-side. But layer 1 — the recursion that compounds everything into a 6.5h billing burn — you can cut deterministically at the root today.

Deterministic user-side guard for layer 1: block a background Agent/Task spawn when you are already inside a sub-agent. In this harness, sub-agent sessions carry CLAUDE_CODE_CHILD_SESSION in the environment (I ran into this variable being inherited into child sessions while investigating a separate transcript-loss issue). A PreToolUse hook on Agent|Task:

#!/usr/bin/env bash
set -euo pipefail
INPUT="$(cat)"
TOOL="$(printf '%s' "$INPUT" | jq -r '.tool_name // empty')"
case "$TOOL" in Agent|Task) ;; *) exit 0 ;; esac
BG="$(printf '%s' "$INPUT" | jq -r '.tool_input.run_in_background // false')"
[[ "$BG" == "true" ]] || exit 0
if [[ -n "${CLAUDE_CODE_CHILD_SESSION:-}" ]]; then
  echo "Blocked: a sub-agent tried to spawn a background agent (the recursion root of #73829). Do the work inline, or spawn background agents only from the top-level session." >&2
  exit 2
fi
exit 0

Wire it via PreToolUse matcher Agent|Task. This stops the nesting at one level: the top-level session can still fan out background agents, but those agents can't themselves fan out more background ones — which is what produced the orphaned sibling tasks here.

What I verified vs. what I'm inferring: I verified the hook's behavior locally by feeding it mock tool-call inputs — it blocks (exit 2) only when the tool is Agent/Task and run_in_background is true and CLAUDE_CODE_CHILD_SESSION is set; it passes (exit 0) for a top-level background spawn and for foreground spawns from inside a sub-agent. I did not reproduce the desktop-app task-panel orphaning myself — that part is from your report and is specific to Windows desktop 1.18286.0.

Because the hook is defense-in-depth rather than a hard guarantee, pair it with a CLAUDE.md line for layer 1: instruct agents to research inline and never spawn further background agents from within a sub-agent.

The real fixes still need to come from the provider side: (a) an agent waiting on its children should idle/quiesce on the completion notification rather than re-invoking itself with no-ops, and (b) descendant tasks should stay inspectable/stoppable from the top-level session (or be cleaned up when their parent's session ends), so a fan-out can't leave orphaned, billing-burning tasks with no stop handle.

blwfish · 1 month ago

Corroborating report from independent transcript instrumentation (not affiliated with this repo, just a personal DB-backed logger of my own Claude Code sessions). Over 2026-06-30 through 2026-07-04, across 8 sessions using the Agent tool (49 launches, unrelated projects), I found 9 instances of the "fabricated background wait" stall your report describes — but in 8 of the 9, no child agent had actually been spawned; the agent just hallucinated the premise and stalled on a non-answer. Only 1 of the 9 matched this issue's exact scenario (a real spawn chain that got orphaned once the parent session ended).

Given that distinction, I filed a narrower companion issue for the no-real-child variant, since your proposed PreToolUse/CLAUDE_CODE_CHILD_SESSION guardrail wouldn't catch it (there's no tool call to gate) — see #74317. Cross-linking in case they turn out to share a root cause on your end.

blwfish · 1 month ago

Cross-linking a downstream symptom this issue doesn't currently mention: the same root cause described here — a subagent spawning further background subagents instead of working inline — also produces a completely different failure signature in at least some cases: a hard server-side rate limit that kills the aggregating agent's turn outright, rather than an infinite no-op loop.

What I hit today: dispatched 5 parallel general-purpose review subagents (manual orchestration, not /deep-research). Several of those first-level agents recursively spawned their own children to cover sub-areas — the exact "an intermediate agent spawns further background agents instead of doing the sub-topics inline" pattern from this issue's summary. In my case that produced a burst of 20+ concurrent API-calling agents from one session, and several of the aggregating parents' turns terminated mid-run with:

API Error: Server is temporarily limiting requests (not your usage limit) · Rate limited

No infinite loop, no orphaned/unstoppable task — just a fast, hard death that destroyed the parent's synthesis step before it could report its children's findings (the children that had already finished still delivered results as separate notifications, so nothing was silently lost, but the aggregation itself was gone).

See also #65731, which independently root-caused and reproduced this exact mechanism for the built-in /deep-research workflow specifically (concurrent subagent fan-out → server-side throttle, confirmed fixed by capping to ≤3 concurrent verifier calls), and #53915's comments, where multiple people independently report "If i use teams of subagents, it just starts spewing these errors." None of these three threads currently reference each other, but they look like the same enabling bug with (at least) three distinct downstream symptoms:

  1. this issue — no-op loop + permanently orphaned/unstoppable task
  2. #69212 — nested results routed to the root session instead of the spawning intermediate agent
  3. #65731 / #53915 / the ~30-issue "Server is temporarily limiting requests" cluster — hard rate-limit death mid-synthesis

All three share the same missing guardrail: nothing currently prevents (or throttles) a subagent from itself spawning further subagents. The workaround proposed in this thread (a PreToolUse hook gating on CLAUDE_CODE_CHILD_SESSION) would plausibly prevent all three symptom classes at once, since it addresses the shared trigger rather than any one manifestation.

Why this is worse than an ordinary bug: it doesn't fail on new or unusual code — it silently wrecks a previously-reliable, already-working multi-agent review/research workflow with zero prompt or code change on the user's side. The only variable that flipped was how much recursive fan-out volume that particular session happened to produce. No warning, no backoff, no partial-completion signal surfaced to the user — just a dead aggregator, lost synthesis, and burned tokens re-deriving what a child agent had already found.

blwfish · 1 month ago

Fresh recurrence of the exact pattern from my comment above, same day: a review subagent spawned a child that itself spawned another child, each just hallucinating "I'll wait for the background agent to complete" with no real work happening. Task notifications kept arriving for ~20 minutes after I'd already bypassed the chain and pulled the needed data directly — resolved with an explicit manual kill, not a timeout or self-recovery.

Separately, the same day's rate-limit variant of this root cause turned out to have a real, measurable cost: see my comment on #65731 just now — the fan-out storm alone burned ~40% more tokens than my entire other day's work combined. Two different failure signatures (infinite no-op loop here vs. hard rate-limit death there), same missing guardrail, same day, same underlying multi-agent workflow.

IgorGanapolsky · 1 month ago

Cross-referencing a pattern from my own agent runs: the recursive spawning problem compounds with cost because each orphaned grandchild keeps its own context window warm, burning tokens even when producing no-ops.

A practical mitigation alongside the workaround above:

  1. Set CLAUDE_CODE_FORK_SUBAGENT=0 at the environment level (not just per-session) to prevent background agents from spawning their own background agents. This caps the fan-out tree at depth 1.
  1. Use a pre-execution gate that checks process tree depth before allowing a new Agent tool call. If the calling process is already a child of a background agent, block the spawn.
  1. For the unstoppable orphan problem: pkill -f "claude.*--background" clears orphaned processes, but you lose in-flight work. A safer approach is to wrap background agent launches in a shell script that traps EXIT and kills the process group.
  1. Token budget enforcement: Set a hard ceiling or monitor via ccusage — kill the session if spend exceeds a threshold. This prevents the 6.5-hour runaway from compounding.

The deeper issue is that TaskStop/TaskOutput lose their handle to descendants when the parent session ends. A fix would be to register all spawned agents in a persistent PID file so orphans can be enumerated and killed even after the parent is gone.

IgorGanapolsky · 1 month ago

This is a well-known failure mode in recursive agent systems — unbounded spawning plus no kill path equals runaway resource consumption. The 6.5-hour runtime is the visible symptom; the root cause is three missing guardrails:

  1. Recursion depth limit: Background agents should not be able to spawn their own background agents without limit. A hard cap (e.g., depth=2) prevents the recursive cascade. If an agent needs deeper delegation, it should do the work inline rather than spawning.
  1. Wall-clock watchdog: Every background agent should have a maximum runtime (e.g., 30 minutes). When exceeded, the watchdog kills the process and marks the result as timed-out. The 6.5-hour loop would have been killed at the 30-minute mark. The watchdog should be external to the agent — the agent cannot be trusted to kill itself, especially when it is stuck in a no-op loop.
  1. Agent registry: Spawned agents should be registered in a durable registry with their PID, session ID, and a kill switch. When the parent session ends, the registry should either kill all children or make them addressable from the top-level session. The "unreachable and unstoppable" state you describe is the worst case — the agent is consuming resources and nobody can stop it.

For the no-op loop specifically: the agent is likely stuck in a pattern of "check if children are done → they are not → make a filler tool call to look busy → check again." A simple heuristic — if the last 5 tool calls produced no state change, force the agent to quiesce (stop making calls, just wait) — would break the loop.

This is the kind of failure that accumulates cost silently. At 6.5 hours of continuous tool calls, the token spend is non-trivial, and nobody is watching because the agent is "in the background."

IgorGanapolsky · 1 month ago

This is a textbook cascading failure. Background agents spawning sub-agents that spawn sub-agents, looping on no-ops for 6.5+ hours — and then becoming unreachable when the parent session ends.

Three things that would prevent this:

  1. Depth limiter: Cap the recursion depth of agent spawning. max_agent_depth = 2 — a background agent can spawn one level of sub-agents, but those sub-agents cannot spawn further. This is the single most effective guard against recursive spawning.
  1. Idle timeout on background agents: If a background agent hasn't produced meaningful output (non-filler, non-no-op) in N minutes, kill it. 6.5 hours of no-ops means there was zero idle detection. A simple heartbeat: if last_useful_output > 15 min: terminate.
  1. Orphan reaper: When a parent session ends, all child agents should be terminated immediately, not left running. The fact that they become "unreachable but still running" means there's no process tree cleanup. A reaper process that kills all PIDs in the agent's cgroup when the parent exits would fix this.

The token burn from 6.5 hours of recursive no-ops must be significant. At Opus pricing (~$15/M tokens), even a conservative 10K tokens/hour of no-op looping = ~$1/hour per orphaned agent. If you had 5 agents running, that's $30+ in pure waste.

For anyone hitting this right now: pkill -f "claude.*background" will kill orphaned background agents. Not graceful, but it stops the bleed.

yurukusa · 1 month ago

Following up on my three-layer breakdown above — a few people have since proposed a pre-execution depth gate (checking process-tree depth before allowing a spawn). That's the right instinct for layer 1, but there's a design detail that decides whether the gate actually holds across the exact boundary this issue is about: the spawning session ending.

I verified this week that a launch-layer PreToolUse guard does reject a background spawn at exit 2 before the tool call runs — but only the next spawn. It has zero reach into a grandchild that is already running, which is the same wall I hit earlier in this thread where TaskStop/TaskOutput return No task found on the orphans. So the gate has to be paired with something that can reach an already-running process, or it just moves the runaway one spawn to the left.

The piece that makes both work is where the in-flight count lives. If the gate counts from session-local state or the live process tree, it under-counts the moment children detach — precisely when the runaway becomes unstoppable. If it counts from a persistent on-disk roster (append on spawn, remove on clean exit), the count stays correct across session boundaries, and that same file gives a reaper a handle to the orphans TaskStop can't touch. Layer-1 prevention and the layer-3 kill-path become two reads of one file.

Two small pieces sharing one roster:

# 1) PreToolUse gate on a background Agent/Task spawn — reject at the launch layer
ROSTER="$HOME/.cache/agent-roster.jsonl"
INFLIGHT=$(grep -c '"state":"running"' "$ROSTER" 2>/dev/null || echo 0)
MAX_INFLIGHT=5
if [ "$INFLIGHT" -ge "$MAX_INFLIGHT" ]; then
  echo "Refusing background spawn: $INFLIGHT already in flight (cap $MAX_INFLIGHT)" >&2
  exit 2   # hard block, before the tool call runs
fi
# on allow: append {"pid":..,"ppid":..,"session":..,"state":"running","ts":..} to $ROSTER,
# and flip state to "done" on a clean Stop hook.
# 2) Reaper (cron, every few minutes) — the gate above cannot kill what is ALREADY
#    running, especially once the parent session is gone. Read the roster and SIGKILL
#    orphans whose owning process no longer exists.
jq -c 'select(.state=="running")' "$ROSTER" 2>/dev/null | while read -r r; do
  ppid=$(jq -r .ppid <<<"$r"); pid=$(jq -r .pid <<<"$r")
  kill -0 "$ppid" 2>/dev/null || kill -9 "$pid" 2>/dev/null   # parent gone -> orphan -> kill
done

The honest limits: (a) the gate in (1) is a launch-layer control — it stops the next spawn, not a cascade already below the tool layer; that is what (2) is for. (b) This does nothing for layer 2 (the no-op busy-loop) — an agent that re-invokes itself with filler tool calls stays within the cap and passes the gate; that one still reads as provider-side to me, since the client can't tell "meaningful step" from "filler" reliably. The roster only closes the recursion-plus-orphan half of the failure, which is the half that produced the 6.5-hour bill here.

kcarriedo · 1 month ago

The recursive spawn pattern you are describing is a known failure mode when the Agent tool does not enforce a depth cap -- the sub-agent reads its own prompt, sees "use background agents for parallel work," and follows the same instruction the parent did.

The 6.5-hour loop of no-ops while waiting for children (Checked current time, idled, Monitor timeout) is particularly expensive because the agent is burning context and quota on filler rather than either doing work or failing cleanly.

A few things that might be useful to add to the report:

  • What was the original prompt given to the top-level background research agent? Specifically, did it include any instruction about parallelization or delegation?
  • After the spawning session ended, was there any way to find the orphaned sub-agent IDs from the task panel, or were they completely hidden?
  • The No task found with ID response from TaskStop -- was the task ID pulled from the transcript of the original conversation, or from the agent view list?

We have been working on supervisor logic for exactly this scenario (orphaned sub-agents that keep running after their parent session exits). The core fix we landed on is tracking the full process group at spawn time and killing the group when the parent session terminates -- without that, sub-agents survive the parent and have no natural stop condition.

Happy to share more detail on what we found if it helps narrow down the root cause here.

Showing cached comments. Read the full discussion on GitHub ↗