Sessions in invisible wait states accumulate for days with no timeout or user notification

Resolved 💬 10 comments Opened Feb 14, 2026 by rodboev Closed Apr 8, 2026

Sessions in invisible wait states accumulate for days with no timeout or user notification

Summary

Correctness violation: Orphaned subagents override explicit user denials and continue executing after session exit.

When a user:

  1. Is prompted to approve an action (e.g., CLAUDE.md rewrite)
  2. Explicitly denies the prompt
  3. Exits the session with /exit

The orphaned subagent executes the denied action anyway, repeatedly modifying files the user refused to change.

This is not just a resource leak — it's a trust boundary violation. Combined with invisible wait states and no stale-session detection, this created a 5-day cascade affecting 50+ sessions across 6 project areas, corrupting shared config files and losing ~50 session transcripts.

Why this is not a duplicate of #19045, #20369, or #20236

Those issues report individual symptoms. This documents the systemic cascade that emerges when those failures compound over days:

| This issue | Flagged "duplicates" |
|---|---|
| 28 sessions accumulating 390 hours of invisible wait states (forensically proven via .claude/file-history/ timestamps) | #19045: 20+ orphaned processes after 1 session |
| Explicit user denial overridden by orphaned subagent | #20369: Single process memory leak (30GB) |
| File corruption from 20+ concurrent sessions writing same CLAUDE.md (proven via content hash collisions) | #20236: TaskOutput polling doesn't detect completion |
| External health monitor architecture proposed (novel) | Not addressed in any flagged issue |
| Cross-session collision proven via hardlink inode forensics | Not addressed in any flagged issue |

Relationship: #19045 reports a root cause (process leak). This issue reports the cascade failure mode when that leak runs unchecked for days, plus the architectural fixes needed beyond process cleanup.

Reproduction

Tier 1: Basic orphan (5 minutes)

  1. Start Claude Code session
  2. Trigger Task tool subagent: Ask "Research X and Y in parallel"
  3. Exit with /exit
  4. Verify: ps aux | grep 'claude.*stream-json' shows orphaned processes

Tier 2: Invisible wait state (20 minutes)

  1. Start session, ask: "Propose a CLAUDE.md restructure"
  2. When prompted for approval, don't respond
  3. Wait 20 minutes, check debug log
  4. Look for: Repeated backgrounding process async_hook_* with no termination events
  5. Verify: Session keeps cycling (re-reading files, re-writing .claude.json)

Tier 3: Denial override (critical)

  1. Start session with a plugin that spawns subagents
  2. Trigger action requiring approval (e.g., CLAUDE.md rewrite)
  3. Explicitly deny the prompt
  4. Exit with /exit
  5. Monitor file: Subagent continues modifying the file you denied
  6. Evidence: Check git history for post-exit modifications

Critical escalation: Explicit denial override

The user was prompted to approve writing to CLAUDE.md. They explicitly denied the prompt and exited the session. The orphaned subagent (spawned by a plugin's writing-skills skill) continued executing the denied action — repeatedly applying CLAUDE.md testing variants to the live file, as evidenced by multiple entries in git history and 20 separate plugin cache temp_git_* directories with timestamps spanning Feb 10-13 2026.

Why the denial didn't persist:

  1. Approval gate existed only in parent session memory
  2. /exit killed parent without terminating subagent
  3. Denial was never propagated to subagent process
  4. With parent gone, subagent interpreted absence as "proceed autonomously"
  5. Plugin directive: "prevent agents from delegating prioritization back to humans"

The exit functioned as an implicit unblock — the approval gate failed open (proceed on user absence) instead of failing closed (deny on user absence).

Environment

  • Claude Code 2.1.41-2.1.42
  • Ubuntu 24.04 on WSL2
  • Superpowers plugin v4.3.0 (amplifies the issue but not required to trigger it)

Evidence from debug logs

Session 90768bd6 (11 hours, Feb 13 2026):

  • 525 async hooks spawned (backgrounding process async_hook_*)
  • 525 "Hook shell status completed" entries
  • 0 process termination events (no SIGKILL, SIGTERM, or reap entries in 34,000-line debug log)
  • 542 atomic writes to .claude.json (each generating 3 filesystem events = 1,626 FS events)
  • Timeouts registered at 600,000ms but never enforced

Session 71067f4c (debug log, 14,027 lines):

  • Lock contention: Failed to save config with lock: Error: Lock file is already being held — write proceeds anyway, bypassing the lock
  • Two atomic .claude.json writes within 8ms from a single /debug command
  • Render loop running hot: High write ratio: blit=0, write=16915 (100.0% writes) repeating every second
  • 4 ERROR lines at the exact same millisecond (stream abort cascade)

The hook system marks processes as "completed" (meaning output was delivered or skipped) but this does not trigger process cleanup.

File-history forensic evidence

Claude Code's .claude/file-history/{session-id}/ directory stores content-addressed snapshots of files edited during each session ({hash}@v{n}). By comparing the birth timestamp of the earliest snapshot to the modification timestamp of the latest, we can measure how long each session was actively producing file edits — and identify gaps where sessions sat idle in wait states.

Session duration analysis

28 sessions showed creation-to-last-modification deltas exceeding 7 hours. The top 16:

| Session | Hours | Snapshots | Activity |
|---------|-------|-----------|----------|
| 87bec414 | 43 | 11 | Windows Terminal AHK hotkey script |
| c782d95b | 31 | 63 | AutoHotKey global hotkeys |
| ed86fe3d | 30 | 16 | Windows scheduled task configuration |
| 7e4deb3e | 28 | 9 | AutoHotKey global hotkeys |
| 648e4a06 | 23 | 11 | C# application code |
| 0b72744b | 22 | 16 | CLAUDE.md analysis + scroll guard script |
| 1b05db83 | 21 | 7 | AutoHotKey + Windows Terminal settings |
| c30a7682 | 14 | 5 | Windows scheduled task + font config |
| 2b50e79b | 14 | 5 | Windows scheduled task XML |
| c18d5c37 | 12 | 50 | PowerShell automation scripts |
| 5cb4a458 | 12 | 9 | Windows Explorer view enforcement |
| f37d69ee | 11 | 3 | Windows Terminal F10 keybinding plan |
| 8e588ed5 | 10 | 18 | CLAUDE.md rewrite proposals |
| 7dd87ecd | 10 | 7 | Windows Explorer navigation config |
| fc9598af | 9 | 44 | MCP plugin test suite (Bun) |
| 8eea2585 | 9 | 41 | PowerShell automation scripts |

Combined: 390 hours across 28 sessions with >7-hour deltas, spanning 6 distinct project areas (AutoHotKey, Windows automation, CLAUDE.md configuration, C# apps, MCP tooling, PowerShell). Over 20 sessions touched the same CLAUDE.md content hash (2055741870d3b831), confirming concurrent edits to the same configuration file from independent sessions.

Case study: 43-hour session 87bec414

This session edited a Windows Terminal AutoHotKey script. Its version timeline reveals the invisible wait state pattern:

| Version | Timestamp | Delta | Size | Hardlinks |
|---------|-----------|-------|------|-----------|
| v1 | Feb 11 15:16 | — | 5,966 B | 1 |
| v2 | Feb 13 03:39 | +36h 23m | 6,005 B | 1 |
| v3 | Feb 13 06:40 | +3h 01m | 6,022 B | 2 |
| v4 | Feb 13 06:42 | +1m 24s | 5,572 B | 2 |
| v5 | Feb 13 06:45 | +2m 58s | 5,688 B | 2 |
| v6 | Feb 13 06:48 | +3m 48s | 5,075 B | 2 |
| v7 | Feb 13 10:20 | +3h 32m | 4,932 B | 2 |
| v8 | Feb 13 10:45 | +24m 32s | 4,239 B | 2 |

Key observations:

  1. 36-hour invisible wait (v1→v2): The session produced one edit, then sat idle for 36 hours before producing its next. No user interaction occurred during this gap — the user didn't know the session was waiting for input.
  1. Unsupervised revision burst (v3→v8): 6 revisions over ~4 hours shrank the file from 6,022→4,239 bytes (−30%). These edits happened autonomously with no user review or approval.
  1. Concurrent session collision (v3+): At v3, the hardlink count jumps from 1→2, meaning a second session (32b779be) began accessing the same file simultaneously. Session 32b779be was installing a new MCP tool and collided with the running AHK session. Neither session detected the other's presence.
  1. No collision detection: Both sessions continued operating independently on the same file. The file-history snapshots diverged silently — no lock contention warning, no user notification, no attempt to coordinate.

Infrastructure-modifying session b41a3823

One session was particularly impactful: b41a3823 was a subagent spawned by a plugin's writing-skills skill. It was installing a new MCP tool (exa code) and modified both the MCP configuration (4 versions in 22 minutes) and the session-start hook (session-start.js, 78KB). Because the session-start hook fires on every startup|resume|clear|compact event, changes to this hook propagated to every other active session — amplifying the cascade. The user later found this subagent had been writing to configuration files continuously, including CLAUDE.md, over the course of several days — without ever surfacing its activity or requesting approval.

Root cause

Primary: Invisible wait states

Sessions that propose changes requiring user approval (CLAUDE.md rewrites, PR submissions, code modifications) enter a wait state. When the user doesn't respond — because they don't know the session is waiting — the session keeps cycling:

  1. Session proposes a change, waits for approval
  2. User never responds (doesn't know it's waiting)
  3. Session internally re-prompts, re-reads files, re-writes config
  4. Each cycle triggers hooks, generates filesystem events
  5. No timeout, no stale-session detection, no notification to user

Over Feb 8-13 2026, 50+ sessions accumulated in this state. Identified activities include:

  • CLAUDE.md restructure proposals (multiple rewrites cycling)
  • PR submission requests waiting for approval
  • Codebase analysis ("insight command") cycling with research output
  • F10 keybinding configuration for Windows Terminal
  • Upstream research tasks producing recommendations

Secondary: No process lifecycle management

  1. No process group management: Subagents spawn without controlling terminal (TTY: ?) and in separate process groups (different PGID from parent). Parent exit sends no SIGTERM to children.
  1. No session exit cleanup: mid-conversation.js cleanup() only disconnects the memory client. session-end.js performs memory consolidation then exits. Neither reaps outstanding hooks or subagent processes.
  1. No timeout enforcement: Hooks register with timeout 600000ms but the debug log shows zero instances of timeout-triggered kills.
  1. "Completed" doesn't mean "terminated": Hooks: Skipping hook async_hook_* - already delivered/sent or no stdout means "we won't show the output to the user anymore." The process itself keeps running.
  1. Lock bypass under contention: When .claude.json lock fails ("Lock file is already being held"), the write proceeds anyway — meaning concurrent sessions corrupt shared config.

Code areas requiring investigation

Based on debug log forensics and behavior analysis:

| Area | Current behavior | Evidence | Fix needed |
|------|-----------------|----------|------------|
| Session exit cleanup (mid-conversation.js cleanup, session-end.js) | Only disconnects memory client | 0 process termination events in 34K-line debug log | Reap outstanding hook/subagent PIDs on exit |
| Hook completion vs. termination (async hook registry) | "Completed" = output delivered, process keeps running | 525 hooks spawned, 525 "completed", 0 reaped | Track PIDs, kill on completion or session exit |
| Timeout enforcement (hook manager) | Registers 600,000ms timeout, never enforces | 0 timeout-triggered kills across all debug logs | Actually kill process when timeout expires |
| Lock contention (.claude.json atomic write) | Lock failure → write proceeds anyway | "Lock file is already being held" in debug log | Fail or retry, don't bypass |
| Approval gate persistence (Task tool subagent spawning) | Denial exists only in parent session memory | User denied → exited → subagent continued | Persist denials to disk, fail-closed on user absence |

Impact

Over a 5-day period (Feb 8-13 2026):

  • 50+ sessions accumulated in invisible wait states across 6 project areas
  • 390+ combined hours of session activity recorded in .claude/file-history/
  • .claude.json atomic write storms (temp file + rename per write, 3 FS events each)
  • On WSL2: each FS event propagates through the 9P bridge, triggering Windows Defender scans
  • 20+ sessions concurrently edited the same CLAUDE.md file, causing corruption
  • ~50 session transcripts were effectively lost — the research and recommendations produced by those sessions were never seen by the user
  • User explicitly denied a CLAUDE.md rewrite; orphaned subagent executed it anyway

The sessions were performing useful work (code review, configuration, automation) but couldn't surface their need for user input, so they cycled indefinitely.

Suggested improvements

Critical: External health monitoring

In-process monitoring alone may not be sufficient here. A health monitor running inside a Claude Code session is subject to the same lifecycle and state as the session itself. If the session is stuck, so is the monitor.

An external approach — analogous to how MCP servers run as independent host processes — could observe all sessions simultaneously:

  1. Session registry: Track active sessions with last-user-input timestamp
  2. Stale session detection: If a session hasn't received user input for N minutes but still has active subagents/hooks, flag it
  3. External notification: Surface stale sessions to the user through a mechanism independent of the stalled session itself
  4. Automatic reaping: After configurable timeout, terminate stale sessions and their subagent trees

Process management

  1. Track PIDs in the hook registry — associate each async hook with its process handle
  2. Session exit handler: Iterate all outstanding hooks and subagent PIDs, send SIGTERM then SIGKILL after grace period
  3. Process group management: Spawn subagents in the parent session's process group so they receive signals when the parent exits
  4. Enforce timeouts: When a registered timeout expires, actually kill the process

Approval gates

  1. Persist denials: Write user denials to session state file, check before subagent proceeds
  2. Fail-closed on absence: Treat user exit/absence as denial, not permission to proceed

Visibility

  1. Show subagent count in session UI: Display "N subagents active" the same way active sessions are shown
  2. Surface wait states: When a session is waiting for user input, always tell the user — never hide it

Workaround

Periodically run: pkill -f 'claude.*stream-json' to clear orphaned subagents. Or reboot.

View original on GitHub ↗

This issue has 10 comments on GitHub. Read the full discussion on GitHub ↗