[BUG] Bash tool dispatch blocks the event loop ~80s in a CPU spin
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?
In a --resumed session with a large context (Sonnet 1M window, effectiveWindow=947000), every Bash tool dispatch synchronously blocks the main thread for ~80–86 seconds before the shell is even spawned. The actual command then completes in ~0.3–0.5s. During the block the TUI is completely dead (spinner not animating, stopwatch frozen, input ignored), the API SSE stream goes unserviced, and MCP SSE connections drop. The process always recovers after the stall — this is not a hang.
The block is an active CPU spin, not a wait: the process's own stall instrumentation reports cpu ≈ wall for every stall, and live /proc sampling during a stall shows the main thread in state R at 100% of one core with zero voluntary context switches for the entire duration.
At the time of writing, one session accumulated 44 event-loop stalls, cumulative 3,685,925 ms (~61 minutes) of frozen UI. 36 of the first 40 Bash dispatches stalled; zero of 53 non-Bash dispatches (Read/Edit/Grep/WebFetch) stalled.
What Should Happen?
The stalls should not hold the process
Error Messages/Logs
Steps to Reproduce
- Session with a very large context (resumed session on a 1M-window model, several hundred k tokens used)
- Any Bash tool call
- → entire process freezes ~80s at 100% CPU before the shell spawns; recovers; repeats on every subsequent Bash call, duration slowly growing with context
Non-Bash tools never trigger it. Fresh/small sessions never trigger it.
Claude Model
Opus
Is this a regression?
Yes, this worked in a previous version
Last Working Version
_No response_
Claude Code Version
2.1.231
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Other
Additional Information
Environment
- Claude Code 2.1.231 (
cc_version=2.1.231.d74/.9fa), native install (~/.local/bin/claude), Bun runtime (threads:mi-scavenger,Bun Pool,JITWorker) - Linux 6.8.0-138-generic x86_64 (Ubuntu 24.04), zsh, plenty of free RAM
- Session:
claude --resume --debug, modelclaude-sonnet-5with 1M context beta (effectiveWindow=947000), large resumed transcript - Stalls occur both on
source=repl_main_threadturns and inside a custom subagent (cc_is_subagent=true,source=agent:custom:*) — the subagent's Bash-heavy loop makes it one freeze per command, back to back
Signature in the debug log
Every stall begins exactly at tool_dispatch_start tool=Bash (back-computing stall start = report time − blocked duration; 25/25 stalls traced land on a Bash dispatch). The block sits after the permission decision (1–3 ms) and before Spawning shell:
05:29:45.016Z [INFO] [Stall] tool_dispatch_start tool=Bash toolUseId=toolu_01E3... permissionDecisionMs=2
... ~80s of nothing ...
05:31:05.278Z [WARN] [event-loop-stall] blocked for 80227ms (expected 200ms, actual 80427ms).
Total stalls: 5, cumulative: 424143ms [likely sleep/wake] cpu=80829ms majflt=0 rss=943MB heap=184MB ext=68MB
05:31:05.3xx [DEBUG] Spawning shell without login (-l flag skipped)
05:31:05.4xx [INFO] [Stall] tool_dispatch_end tool=Bash ... outcome=ok durationMs=80447 <-- actual exec ≈ 0.3–0.5s of this
Notes on the log evidence:
cpu=80829msfor an 80.2s wall block on every stall → active spin. The[likely sleep/wake]label the stall detector attaches is a misclassification (minor secondary bug — the heuristic apparently doesn't check the CPU counter it prints).- Stall duration crept from 80.2s → 86.6s as the session grew, suggesting the hot work scales with transcript/context size.
- The first stall of the resumed session (101s) happened during request assembly, between
Dynamic tool loading: found 4 discovered tools in message historyand4/42 deferred tools included— i.e. inside a message-history scan, before any Bash call. All subsequent stalls anchor to Bash dispatch. - One 168s stall covered two back-to-back Bash dispatches.
- Collateral in the log (effects, not causes):
Streaming stall detected: ~80s gap between eventson nearly every affected turn (SSE events sat in the socket buffer while the loop was blocked); MCP server SSE connection drops every stall (SSE stream disconnected ... Terminal connection error 3/3 → Closing transport, then reconnect);[Stall] stream_idle_partialwatchdog warnings.
Live process evidence (sampled during a stall)
Main thread of the running CLI (PID sampled at 2s intervals mid-stall):
state=R ut=1160.3 st=1902.2 wchan=0 voluntary_ctxt_switches=242773 nonvoluntary=263370
state=R ut=1161.1 st=1903.4 wchan=0 voluntary_ctxt_switches=242773 nonvoluntary=263500
state=R ut=1162.5 st=1906.0 wchan=0 voluntary_ctxt_switches=242773 nonvoluntary=263850
state=R ut=1164.1 st=1908.5 wchan=0 voluntary_ctxt_switches=242773 nonvoluntary=264190
- ~100% of one core: ~0.38 s/s user + ~0.63 s/s system time — roughly two-thirds kernel time
voluntary_ctxt_switchescompletely frozen for the whole stall → the thread never blocks on I/O, locks, or sleeps; it stays runnable in a tight loop of non-blocking, syscall-heavy work (pattern is consistent with heavy allocation/madvisechurn — RSS oscillates 535 MB ↔ 990 MB between stalls while JS heap stays at ~135–225 MB)- Lifetime totals for the main thread after ~1h40m: 18 min user + 30 min kernel CPU
majflt=0throughout — no swapping/paging
Ruled out
- Endpoint security (Microsoft Defender for Linux /
wdavdaemon): its CPU counters do not move at all during stalls - Memory pressure:
majflt=0, 62 GB machine, flat behavior - Network/VPN: the stall is CPU-bound; API first-byte times are ~1s when the loop is live
- Hooks:
Hooks: Found 0 total hooks in registry - Per-command cost: identical ~80s regardless of the command; the same build on the same machine in a small-context session dispatches Bash in milliseconds
Related (but distinct) issues
- #88257 — recoverable event-loop stall + CPU burn, but first-prompt-only and reproduces in a tiny repo
- #89062 — 100%-CPU main-thread loop on 2.1.231+, but permanent (never recovers) and makes no syscalls; this one recovers every time and is ~2/3 kernel time
Attachment
Full --debug log of the affected session attached (sanitized: username, employer-internal hostnames, and project name replaced; no other lines altered — all [Stall]/[event-loop-stall] lines verbatim).