Bug Report: Background subagent state sync — zombie 'running' agents cause stop hook infinite loop after all subagents have terminated

Status Fixed / completed
Reported on v2.1.131
Maintainer reply ✓ Yes — bogini
Activity 15 comments · opened May 13, 2026 · closed May 27, 2026
💡 Likely answer: A maintainer (bogini, collaborator) responded on this thread — see the highlighted reply below.

Summary

When spawning multiple background subagents (≥6) in a single turn, the session registry fails to update subagent status from "running" to "completed" after all subagents have actually terminated. This causes the stop hook to block turn completion indefinitely, creating an infinite loop that consumes context until forced compaction.

Environment

  • Claude Code version: 2.1.131
  • Session type: compacted continuation (but original spawning occurred before compaction)

Steps to Reproduce

  1. In a single turn, spawn 6+ background subagents using the Agent tool with run_in_background: true
  2. Wait for all subagents to complete (files written to disk, processes exited)
  3. Main agent attempts to end turn

Expected Behavior

Stop hook recognizes all subagents have terminated, allows turn to end cleanly.

Actual Behavior

Stop hook repeatedly blocks turn ending with:

Stop hook feedback:
Background subagents are still running.
Use TaskOutput task_id="..." block=true to wait for their results before ending your turn.

This repeats indefinitely even after:

  • All subagent processes have exited
  • All output files have been written
  • Subagent metadata files exist on disk

Evidence from Affected Session

  • Subagents spawned: 9 (confirmed by 9 agent-*.meta.json files in session subagent directory)
  • Stop hook blocks: 154 occurrences over ~4.5 hours (first at line 265, last at line 1199 of the JSONL transcript)
  • Time window: 2026-05-13T05:07:07 → 2026-05-13T09:27:04 UTC
  • Result: Context exhaustion → forced /compact, session lost mid-task context

Analysis

The root cause appears to be in the subagent lifecycle management:

  1. Subagent processes terminate correctly (files written, no zombie processes)
  2. The session registry's in-memory state table does not receive (or drops) the "completed" status update for one or more subagent entries
  3. Stop hook reads the stale registry state, sees entries still marked "running"
  4. Main agent cannot force-clear or override the stale entries
  5. Loop continues until external intervention

Additional Impact

After compaction, the stale "running" entries persist into the continued session, causing the same stop hook to fire even in the new session where no background agents were spawned.

Suggested Fix

  1. Add a timeout mechanism — if a registered background subagent has not produced output for N seconds, auto-mark as completed/stale
  2. Allow main agent to acknowledge-and-dismiss stale entries via TaskOutput or a new TaskStop variant
  3. Ensure subagent termination events are synchronously propagated to the session registry before the subagent process exits

View original on GitHub ↗

15 Comments

github-actions[bot] · 3 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/48470
  2. https://github.com/anthropics/claude-code/issues/58457
  3. https://github.com/anthropics/claude-code/issues/55893

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

battlesym · 3 months ago

Thanks for the duplicate detection. I've reviewed all three referenced issues carefully, and while they share surface-level symptoms with what I reported, I believe #58637 captures a distinct and more severe variant that warrants separate tracking.

Similarities acknowledged

| Issue | Shared symptom |
|-------|---------------|
| #48470 | Stale subagent IDs block session end |
| #58457 | Background agent tasks remain 'running' after completion |
| #55893 | Stuck tasks survive across session boundaries |

Key differentiators that make #58637 distinct

1. Trigger threshold: parallelism collapses state tracking

#58637 requires ≥6 background subagents spawned in a single turn to trigger. The other issues occur with lower parallelism or foreground agents only. This suggests a race condition or slot-management bug in the task registry under concurrent lifecycle events, not a general stale-ID leak.

2. Consequence severity: infinite loop until context exhaustion

The stop hook blocked 154 times over 4.5 hours, consuming the entire context window until forced compaction. This is not "can't stop cleanly" (#48470) or "UI shows wrong state" (#58457) — it's session-level denial of service. No manual intervention (/) was documented as working in those sessions.

3. Persistence across compaction

After (forced by context exhaustion), the stale "running" entries persisted into the new continued session, causing the same stop hook to fire immediately. The other issues do not report cross-compaction survival. #55893 is cross-session but via OS processes (PID leaks), not via runtime state that should be reconstructed from scratch on compaction.

4. The root cause is in the runtime's subagent state table, not the UI layer or bash polling

  • Not a UI rendering issue (#58457) — the stop hook genuinely reads stale registry state
  • Not a bash polling loop pattern (#55893) — no polling loops were involved; all subagents completed and wrote their output files
  • Not a foreground agent race (#48470) — all 9 spawned subagents were background ()

The fix needed here is specific: the session registry's in-memory state table must synchronously propagate 'completed' status for all subagent slots before declaring the turn ready to end. Timeout-based GC or a force-dismiss mechanism would address this case, whereas the other issues have different root causes (UI state sync for #58457, bash PID reaping for #55893).

I'd recommend treating this as a related but escalated variant rather than closing as duplicate. Happy to consolidate if there's a plan to track these collectively under #48470 with an updated title/scope that covers the ≥6 parallelism trigger and compaction persistence.

JordanGeogheganMGTS · 3 months ago

Adding a confirming instance of this bug.

Trigger: 4 background subagents spawned in a single turn to push files to GitHub via MCP
Symptom: One subagent ("Push batch 3 of 4 files to GitHub main") became stuck and has persisted across session boundaries, browser restarts, and app restarts indefinitely
Stop button: Clicking Stop causes immediate resumption — the task cannot be terminated by any user-accessible means
Confirmed not a local process: ps aux in a live session shows no trace of the stuck task — it is running server-side
Platform: Claude Code Web (claude.ai/code), Chrome browser, Claude Code version 2.1.140
The session the task originated from has long since ended. The task appears to be surviving in the server-side runtime state table as described in this report.

tommy29tmar · 3 months ago

I’d separate three states here: process lifetime, registry status, and model-visible task state. The stop hook seems to be trusting a stale registry row after the process/artifact layer says the subagents are done.

Good acceptance tests: spawn 9 background agents, let all write final artifacts, then force the registry to miss one completion event. The turn should not loop forever; it should mark the row stale_orphaned, expose the last heartbeat/artifact path, and let the parent decide whether to adopt/cancel. A compact/continue test should also verify the handoff capsule includes each subagent id, final artifact, status source, and next action.

The key invariant: stop hooks can block on live work, but they need a bounded escape path when the only remaining evidence is stale metadata.

junaidtitan · 3 months ago

Zombie subagents inflating context until forced compaction is a nasty loop. Cozempic's guard daemon watches for exactly this — it detects oversized sessions from runaway agents and applies graduated pruning before context hits the wall, plus a final checkpoint on exit even if the Stop hook fails. pip install cozempic — auto-wires on first run. https://github.com/Ruya-AI/cozempic — happy to hear if it helps with the infinite loop scenario.

junaidtitan · 3 months ago

154 stop hook blocks over 4.5 hours from zombie subagents is painful. Cozempic (https://github.com/Ruya-AI/cozempic) has Agent Teams protection that understands subagent boundaries in session JSONL and a zombie-teams doctor check specifically for detecting orphaned/stale agent state. It also prunes completed subagent transcripts via its strategies while preserving active ones. pipx install cozempic && cozempic doctor to audit the session for zombie team state.

junaidtitan · 3 months ago

Zombie subagents causing an infinite stop-hook loop is a nasty one — especially when it burns context until forced compaction. Cozempic has specific Agent Teams protection that preserves valid team coordination messages while pruning the bloat that accumulates from subagent transcripts. The guard daemon also monitors the Claude PID and does a final checkpoint on exit, which helps with exactly the "all agents terminated but session thinks they're running" scenario. pip install cozempic && cozempic guard — curious if it helps prevent the context burn from the loop.

junaidtitan · 3 months ago

Zombie subagents eating context in an infinite loop is painful. Cozempic has specific Agent Teams protection — it preserves active team coordination messages while pruning the bloat that accumulates from subagent transcripts. The guard daemon's watchdog also monitors the parent Claude PID and does a final checkpoint on exit even if the stop hook fails. pip install cozempic && cozempic guard to run alongside your sessions. Repo: https://github.com/Ruya-AI/cozempic — curious if the team-aware pruning helps with the state sync issue.

battlesym · 3 months ago
Zombie subagents eating context in an infinite loop is painful. Cozempic has specific Agent Teams protection — it preserves active team coordination messages while pruning the bloat that accumulates from subagent transcripts. The guard daemon's watchdog also monitors the parent Claude PID and does a final checkpoint on exit even if the stop hook fails. pip install cozempic && cozempic guard to run alongside your sessions. Repo: https://github.com/Ruya-AI/cozempic — curious if the team-aware pruning helps with the state sync issue.

Thanks, bro. Thanks for your advice and the project you recommended. I checked the project and it does seem to address some of my issues. However, the problem I'm facing is essentially a bug in the runtime state registry of Claude Code. Cozempic is just a workaround on the outside. Normal use can also restore the conversation through the /compact command. I think I'll wait for the official update from Claude Code to solve this problem. Thanks for your suggestion. But I think your four comments of the same meaning under the issue I reported might distract Claude's official attention from this issue. Could you please delete some and keep just one of your comments? Thanks for your cooperation.

battlesym · 3 months ago

Additional reproduction + /compact connection

I'm adding diagnostic data from a session that hit this same bug today. My findings add two new dimensions: the /compact trigger path and the "ghost b-prefix task" phenomenon.

Environment

  • Windows 10, Claude Code v2.1.131
  • No custom stop hooks configured — the blocking message is from the built-in callback

Trigger: Large ingest pipeline (18 subagents across ~12 turns)

The session spawned 18 total subagents (3 foreground + 15 background in batches of ≤3) across ~12 turns to perform a wiki knowledge-base ingest. All agents completed successfully and all 35 output files were verified. But after the final turn, the stop hook began reporting "Background subagents are still running" — and never stopped.

/compact does NOT fix it — it makes it permanent

After /compact:

  • 18 stale agent-*.meta.json files remained in subagents/ — not cleaned by compaction
  • 18 stale agent-*.jsonl files remained — same
  • The harness loaded these into an in-memory agent registry at session init
  • After that point, disk cleanup (deleting meta.json, jsonl, tasks/*.output) had zero effect — the registry is memory-resident and never re-read from disk

Ghost b-prefix task IDs

After the registry became corrupted, every subsequent tool call generated a new b-prefix ghost task (e.g., bcp5rx3n0, bakh2erqx, bd0gha4fm) with 0-byte output files in the temp tasks directory. These ghost tasks appeared to be the harness misidentifying regular tool calls as background agent spawns.

Exhausted recovery attempts (all failed)

| Attempt | Result |
|---------|--------|
| Delete meta.json from subagents/ | No effect (memory cache) |
| Delete jsonl from subagents/ | No effect |
| Write "status": "completed" to meta.json | No effect |
| TaskStop on all 18 agent IDs | "No task found" (stop hook registry ≠ task system registry) |
| TaskOutput on all agent IDs | "No task found" |
| Delete all task output files | No effect |
| Delete entire session directory | No effect (already in memory) |
| Spawn new background task → complete → TaskOutput | New task completes, old entries persist |
| Delete both .claude/projects/<session>/ and Temp/claude/<session>/ | No effect |
| EnterPlanModeExitPlanMode | AbortError |
| ScheduleWakeup | Rejected |
| CronCreate | Not tried (user interrupted) |

Only recovery: kill process

The only way out was to kill the claude.exe process and restart. /compact was not sufficient because the in-memory registry survived compaction.

Root cause hypothesis

Two separate data structures track subagent state:

  1. Task system (accessible via TaskOutput/TaskStop) — correctly cleaned up
  2. Stop hook registry (internal, not tool-accessible) — counter drifts after ≥10 agents

When the stop hook checks for running subagents, it queries registry #2, which is out of sync with registry #1. This explains why TaskOutput returns "No task found" for all IDs while the stop hook simultaneously reports them as running.

The b-prefix ghost tasks suggest the corruption spreads beyond just the counter — the spawn-detection logic itself becomes broken, tagging regular tool calls as agent spawns.

Suggestions

  1. /compact should either clean subagents/ directory or mark all entries complete
  2. Stop hook should cross-reference with the task system, not maintain an independent registry
  3. Add a timeout: if a subagent ID has no corresponding task in the task system for > 60s, remove it from the stop hook registry
battlesym · 3 months ago

After further analysis, I want to clarify the scope of the fix needed here. I initially filed #58731 suggesting /compact should clean up the subagents directory — but on reflection, that's the wrong layer to fix. /compact compresses conversation history; it shouldn't be a garbage collector for transient runtime state. This comment reframes the fix options with the right architectural boundaries.

A note on session-init filtering (do not do this)

One intuitive idea is: "when the session reloads after /compact, filter out 'stale' agent entries from disk." This is dangerous in the multi-agent long-running task scenario:

If compaction fires while some subagents are still running (#4, #5, #6 from the spawn batch), session init has no reliable way to distinguish "completed but registry missed the event" from "still running, don't touch me." Both are on disk as the same meta.json file without a reliable status field. This filtering would silently kill active subagents and break the parent agent's ability to collect their results. Avoid this approach.

Viable fix approaches (ordered by where they target)

Approach 1: Fix completion event propagation

What: When a subagent's executor process actually terminates, ensure the "completed" event is synchronously propagated to the session registry before the registry is queried again.

Affects: Runtime subagent lifecycle manager / IPC layer.

| Pros | Cons |
|------|------|
| Fixes the root cause | Highest implementation complexity |
| No semantic change to existing behavior | Requires changes to the event/pipe layer |
| Does not affect multi-agent workflows — only completed agents are cleared | Longer to ship |

This is the correct long-term fix but the hardest to implement.

Approach 2: TaskOutput as implicit acknowledgment

What: When the main agent calls TaskOutput(id, block=true) and receives the result, the registry implicitly marks that agent entry as "acknowledged" or "consumed." The stop hook ignores acknowledged entries.

Affects: TaskOutput return path + registry status update.

| Pros | Cons |
|------|------|
| Perfectly aligns with multi-agent task semantics (plan → distribute → collect results → validate) | Doesn't help if the main agent never calls TaskOutput (e.g., spawned-and-forgot agents) |
| Minimal side effects — only marks after the parent explicitly consumed the output | — |
| Survives compaction naturally — if the main agent re-calls TaskOutput after reload, it still works | — |

This is the most "correct by design" approach for the workflow described in this report (main agent spawning subagents, collecting results, and validating them).

Approach 3: Stop hook loop detection + auto-release

What: Track stale agent IDs across consecutive stop-hook invocations. If the same set of IDs blocks N times in a row (e.g., >= 3), auto-dismiss them and allow the turn to proceed. This prevents the 154-loop context-burn scenario from recurring.

Affects: Stop hook only — no registry changes.

| Pros | Cons |
|------|------|
| Lowest implementation cost | Does not fix the root registry sync issue |
| Zero risk of misidentifying active agents (only triggers after repeated identical blocks) | — |
| Directly solves the "154 loops over 4.5 hours" severity in this report | — |

This is the pragmatic "damage control" fix — it doesn't solve why the registry is stale, but it ensures a stale registry can't kill a session.

Recommended fix stack

This stack gives incremental value at every layer without overloading /compact or risking active-agent misidentification.

kcarriedo · 3 months ago

Saw this and it matches a failure mode we hit when we tried to run >5 background subagents from a single orchestrator turn — the "running" → "completed" transition is harness-internal state that the parent has no independent way to verify, so a missed transition wedges the stop hook with no recovery short of /clear.

What's worked for us on a separate runner is treating subagent lifecycle as out-of-process:

  • Each spawned agent gets a cycle_id (UUIDv7) and writes a small started_at / completed_at record to disk before/after work. The orchestrator reconciles from those files, not from the harness's session registry.
  • A heartbeat task renews a lock file with an owner_token while the work is in flight. If the lock expires (TTL exceeded with no renewal), the orchestrator treats that worker as dead — even if the harness still shows "running" — and any subsequent SendMessage/stop signal is a no-op.
  • On compaction, the on-disk state survives, so the stop hook can ask "are there actually live workers?" by checking lock TTLs instead of trusting the in-memory registry.

It doesn't fix the underlying harness bug, but it neutralizes the infinite-loop blast radius: stop hook gates on real liveness, not registry liveness. Reference impl is in gtm-polling-service/src/lock.rs + cycle.rs if useful — happy to walk through the lock-renewal heartbeat in detail.

(One related sharp edge: compare-and-delete on release. If you ever just unlink the lock file, you race against a re-spawn that picked up the same slot. Always verify the owner token still matches the one you wrote.)

bogini collaborator · 3 months ago

Fixed in v2.1.143 — consecutive Stop-hook blocks are now capped to prevent runaway loops. Please reopen if you still see this on a current version.

battlesym · 3 months ago

Thanks for closing this in v2.1.143, bogini. The stop-hook block cap is a pragmatic fix that directly prevents the 154-loop context-burn scenario I reported.

However, after deeper analysis, I want to flag a vulnerability that the current design introduces for legitimate long-running background subagent workflows:

The problem: CAP doesn't distinguish between stale-registry blocks and real-liveness blocks

CLAUDE_CODE_STOP_HOOK_BLOCK_CAP counts every {"decision": "block"} from the stop hook. If a stop hook is set to block while background subagents are running, and a legitimate batch task takes many turns to complete, the CAP accumulates across turns and will force-kill the session at count 8, even when subagents are genuinely alive and working.

This is the opposite failure mode from the original bug:

| Original bug | New risk |
|---|---|
| Stale registry never updates → infinite loop | Legitimate slow work accrues block count → premature termination |

The CAP treats "subagent state not synced" and "subagent genuinely still running" identically, because it only counts blocks, not the reason for the block.

Mitigation suggestions (not asking to reopen, just noting)

  • Document the risk: The release notes could mention that long-running background subagent tasks may exceed the default CAP=8, and suggest raising it for such workflows.
  • Consider a smarter counter in a future iteration: The CAP could track whether the same set of stale agent IDs is causing repeated blocks (our original approach 3), rather than counting any block. This would let legitimate progress continue while only breaking true deadlocks.

For now, users with long-running background subagent pipelines should set CLAUDE_CODE_STOP_HOOK_BLOCK_CAP high enough to accommodate their expected turn count, or switch critical subagents to foreground mode where the CAP doesn't apply (since the turn never ends during foreground agent execution).

github-actions[bot] · 1 month ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.