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
Workaround ✓ Mentioned in thread ↓
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
- In a single turn, spawn 6+ background subagents using the Agent tool with
run_in_background: true - Wait for all subagents to complete (files written to disk, processes exited)
- 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.jsonfiles 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:
- Subagent processes terminate correctly (files written, no zombie processes)
- The session registry's in-memory state table does not receive (or drops) the "completed" status update for one or more subagent entries
- Stop hook reads the stale registry state, sees entries still marked "running"
- Main agent cannot force-clear or override the stale entries
- 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
- Add a timeout mechanism — if a registered background subagent has not produced output for N seconds, auto-mark as completed/stale
- Allow main agent to acknowledge-and-dismiss stale entries via
TaskOutputor a newTaskStopvariant - Ensure subagent termination events are synchronously propagated to the session registry before the subagent process exits
15 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
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
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.
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.
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.
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.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-teamsdoctor 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 doctorto audit the session for zombie team state.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.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 guardto 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.
Additional reproduction +
/compactconnectionI'm adding diagnostic data from a session that hit this same bug today. My findings add two new dimensions: the
/compacttrigger path and the "ghost b-prefix task" phenomenon.Environment
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.
/compactdoes NOT fix it — it makes it permanentAfter
/compact:agent-*.meta.jsonfiles remained insubagents/— not cleaned by compactionagent-*.jsonlfiles remained — sameGhost 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 ||
TaskStopon all 18 agent IDs | "No task found" (stop hook registry ≠ task system registry) ||
TaskOutputon 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>/andTemp/claude/<session>/| No effect ||
EnterPlanMode→ExitPlanMode| AbortError ||
ScheduleWakeup| Rejected ||
CronCreate| Not tried (user interrupted) |Only recovery: kill process
The only way out was to kill the
claude.exeprocess and restart./compactwas not sufficient because the in-memory registry survived compaction.Root cause hypothesis
Two separate data structures track subagent state:
TaskOutput/TaskStop) — correctly cleaned upWhen the stop hook checks for running subagents, it queries registry #2, which is out of sync with registry #1. This explains why
TaskOutputreturns "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
/compactshould either cleansubagents/directory or mark all entries completeAfter 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.
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:
cycle_id(UUIDv7) and writes a smallstarted_at/completed_atrecord to disk before/after work. The orchestrator reconciles from those files, not from the harness's session registry.owner_tokenwhile 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 subsequentSendMessage/stop signal is a no-op.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.rsif 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.)
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.
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_CAPcounts 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)
For now, users with long-running background subagent pipelines should set
CLAUDE_CODE_STOP_HOOK_BLOCK_CAPhigh 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).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.