Tool dispatch stalls silently in 2.1.121–2.1.123: tool_use emitted, no tool_result, no disk side-effects, no errors
Tool dispatch stalls silently in 2.1.121–2.1.123: tool_use emitted, no tool_result, no disk side-effects, no errors
Summary
In claude CLI versions 2.1.121, 2.1.122, and 2.1.123 (Claude Code), local tool calls (Write, Bash, Edit, Read) intermittently stall after the model emits a tool_use block. The tool handler is never invoked: no disk I/O occurs, no tool_result is emitted, no permission prompt fires, and no error is logged on the CLI side. The stall persists indefinitely from the CLI's perspective; only an external watchdog can recover the subprocess.
This appears to be a regression. We have not observed the stall on 2.1.120 or earlier in the same workload.
Environment
- OS: macOS 25.4.0 (Darwin)
- Node: (default; can supply on request)
- Install path:
/opt/homebrew/bin/claude→@anthropic-ai/claude-code - Versions where observed: 2.1.121, 2.1.122, 2.1.123
- Last known good: 2.1.120
- Invocation:
claude --print --permission-prompt-tool stdio(subprocess invoked from a parent harness that responds to permission prompts on the CLI's stdio channel)
Signature (how to detect the bug from logs)
Three coincident facts in a single CLI subprocess turn:
- The session jsonl at
~/.claude/projects/<project>/<sessionId>.jsonlcontains an assistant message with atool_useblock ({"type":"tool_use","id":"toolu_…","name":"Write",…}). - No matching
tool_resultentry for thattool_use_idappears anywhere later in the same jsonl. - The intended side-effect of the tool did not happen — e.g. for
Write, the targetfile_pathdoes not exist on disk (or exists only with content from an unrelated later attempt).
There is no accompanying error on stderr, no permission-prompt-tool error, and no Tool permission stream closed before response received message. The CLI gives no observable signal that anything has gone wrong; from the assistant's perspective the call is simply pending forever.
Empirical incidence
In a single host running real interactive workloads:
| Date | Distinct stalled tool-uses |
|------|----------------------------|
| 2026-04-26 (CLI 2.1.120) | 0 (different bug class observed — see "Distinguishing from prior bug" below) |
| 2026-04-27 (CLI 2.1.120 → 2.1.121) | 0 |
| 2026-04-28 (CLI 2.1.121 → 2.1.122 → 2.1.123) | 1 |
| 2026-04-29 (CLI 2.1.123) | 11 |
Tool distribution across the 12 events: Write ×6, Bash ×3, Edit ×2, Read ×1. No correlation with input size or content type — Read ×1 was a small local file; Write ×6 ranged from short text to multi-KB content. No correlation with concurrent MCP load that we can see.
Reproducer status
We have not been able to reproduce the stall with a minimal claude --print "Write hello to /tmp/x"-style invocation. Out of two minimal-repro attempts on 2026-04-29, both completed cleanly in ~9s. The stall appears to require some combination of: a real model turn with prior context, MCP servers loaded, multiple back-to-back tool calls, or model latency above some threshold. We have not isolated the trigger.
If your team can reproduce with a constructed prompt that emits multiple tool calls under MCP load, that would likely be faster than my isolation effort. Happy to provide:
- Sanitized session jsonl excerpts containing the orphan
tool_useblocks and the surrounding turn structure - Full gateway-side log of all 12 stalled events with timestamps and tool IDs
- Our MCP server config (raven, linear, openclaw, framer, outseta)
Distinguishing from the prior perm-stream bug
We previously observed a different hang class on 2.1.120 where the --permission-prompt-tool stdio stream broke and subsequent tool calls returned is_error: true with body Tool permission stream closed before response received. That class appears patched on 2.1.121+ — zero occurrences of that error string in any session jsonl or gateway log since 2026-04-27. The current dispatch stall is silent (no error string, no perm-stream traffic at all), affects the same tools, and has comparable wall-clock impact.
We're not claiming causation, but the timing is suggestive: the perm-stream class disappears in 2.1.121 and the dispatch-stall class appears the same week. May or may not be related changes.
Workaround in place
External watchdog: parent process tracks tool_use start times and aborts the CLI subprocess when any tool exceeds a configurable max age (default 5 min, currently tuned to 10 min general / 240 s for "fast" tools like Read). On abort, the next user message starts a fresh CLI subprocess and recovers cleanly.
This bounds the loss but doesn't fix the bug. Each incident still costs:
- 1 full conversational turn discarded
- ~5 min wall-clock wait
- Whatever the tool was supposed to do (writes lost, edits lost)
- Partial-output orphan in the assistant stream
At 11 incidents on 2026-04-29 alone, that's ~55 min/day of wasted runtime and ~11 user-visible dropped tool calls per day on this one host.
Asks
- Triage: confirm whether anyone else is seeing this on 2.1.121+ and whether you have an internal repro.
- If actionable: point me at a code path or a diagnostic flag I can flip to surface the stall on the CLI side. I'd much rather catch it inside the CLI than from a parent watchdog.
- If a fix lands: I'll re-run the same watchdog-instrumented workload and report incidence at the new version. Trivial to verify.
Happy to provide additional artifacts. Thanks for the work on Claude Code — it has been excellent overall, this is a noisy edge.
---
Reporter context: this issue is filed by a power user running Claude Code as a subprocess of a custom harness. All tool_use IDs cited above are from real internal sessions; full logs available on request via private channel if helpful.
Showing cached comments. Read the full discussion on GitHub ↗
12 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
This is execution-observability failure at the tool-dispatch boundary.
If a
tool_useis emitted but notool_resultarrives, no disk side-effects occur, and no error is surfaced, then the system has entered a silent failure state right at the point where operators need positive evidence that work actually happened.Why it matters:
I would want clarity on:
tool_useeventsdispatch stalledstate instead of leaving users to infer failure from silenceAgent execution needs explicit liveness at the tool boundary. If
tool_usecan go dark with no result, no side-effect, and no error, the operator loses the ability to trust the action surface at all.Thanks @github-actions for the auto-classification. After reading the cited candidates, none of the three are this bug — but the bot missed #51927, which is very likely the same bug class.
On the cited candidates
tool_resultis lost on its way back. Return-path failure. Different code path from this report.Mode Bwas fixed in the v2.1.113 era. The reporter spun out #51927 to track the survivingMode Afailure.On #51927 (not in the bot's list, but the closest match)
That is identical to the signature in this report. Differences are surface-level:
| | #51927 | This report |
|---|---|---|
| Platform | Ubuntu 24.04 on EC2 | macOS Darwin 25.4.0 |
| Launch | headless
--dangerously-skip-permissions| parent harness with--permission-prompt-tool stdio|| Tools affected | Edit (specifically
.claude/skills/**) | Write / Bash / Edit / Read indiscriminately, any path || Versions | v2.1.114 – v2.1.117 | v2.1.121 – v2.1.123 |
The shared core:
tool_useemitted, notool_result, target untouched on disk, session unrecoverable without external watchdog. Trigger surface in this report is wider (any tool, any path) and the affected version range is later, suggesting the bug survived through additional CLI releases on macOS.On the disk-state discriminator (also answers @0xbrainkid's question 1)
This report's empirical work — sampling 5 stuck Writes against disk on 2026-04-29 — found 4/5 target files do not exist, and the 5th existed only from an unrelated later retry with different content. That places the stall before tool invocation, not during transport, not after handler start. The dispatch IPC inside the CLI subprocess is not handing the call to the tool handler. That's why this is filed as a strictly-distinct class from
tool ran, result lost(#53862, #44596).Triage suggestion
If Anthropic prefers consolidation, this and #51927 can be merged as one cross-platform dispatch-stall ticket spanning Linux+macOS, v2.1.114 onward. If you'd rather keep them separate (different platforms, different trigger surfaces, different version ranges), keeping this open as the macOS+harness variant is also fine.
Either way — not a duplicate of #50727, #53862, or #44596 as the bot suggested.
Filing related observations as a master issue at #54891 — same silent failure class observed across multiple Cowork tools (write tools returning success but disk unchanged,
Readtool serving cache instead of disk content,Edittool appending NULL bytes, etc.).The dispatch stall pattern reported in this issue matches Issue #11 in the master ticket: Gmail MCP
create_drafthung mid-call for 20+ minutes with "tool permission stream closed before response received" as eventual error. Worker session became unresponsive andsend_messagecould not recover it.For the engineering team — additional reproduction details and an internal "Iron Rule 7" workaround (bash heredoc + cp + direct disk verification) are documented in #54891. Happy to provide environment captures or specific test cases if useful.
Cross-confirming from a different platform — macOS + claude-code CLI.
@aiken884's master ticket #54891 lists this report as "Issue #11" (dispatch stall). Adding macOS-side forensic data here so triage has both vantage points.
Platform divergence vs #54891
| | This report | #54891 |
|---|---|---|
| Client | claude-code CLI 2.1.121–2.1.123 | Cowork desktop |
| OS | macOS Darwin 25.4.0 | Windows 11 |
| Tool surface affected | Write / Bash / Edit / Read | same + Obsidian MCP, Gmail MCP, dispatch |
| Volume | 22 dispatch-stall events in 24h on a single host | 10+ silent failures over 2 days |
The same silent-failure fingerprint surfaces on two completely independent stacks. Strengthens the "shared upstream cache/dispatch layer" hypothesis aiken884 raises.
Disk-state forensic data (specific to this report)
On 2026-04-29 we sampled 5 stuck Write events from our gateway log against disk state:
| Stuck at (PDT) | Target | On disk after stall? |
|---|---|---|
| 10:38 |
~/.openclaw/workspace/memory/feedback_critique_demands_inline_evidence.md| ❌ missing || 11:20 |
~/.openclaw/workspace/memory/project_ibkr_docker_migration.md| ⚠️ exists, but from an unrelated later retry with different content || 12:18 |
~/.openclaw/workspace/memory/project_ibkr_gateway.md| ❌ missing || 13:57 |
~/.claude/statusline-command.sh| ❌ missing || 14:06 |
~/.claude/statusline-command.sh| ❌ missing |4 of 5 targets simply don't exist on disk after the stall. No
tool_resultentry for any of these tool_use IDs appears in the session jsonl. No "Tool permission stream closed" errors accompanied these stalls.This places the macOS-side stall before tool invocation, not "tool ran but result lost." Slightly different signature than #54891's Issue #2 ("write returns success, mtime unchanged") — both are silent-failure, but the macOS dispatch appears to fail at a different layer than the Cowork write-cache path.
Path-correlation observation
All 4/5 missing targets are paths under
~/.openclaw/workspace/memory/— which is a symlink to~/.claude/projects/-Users-openclaw--openclaw-workspace/memory/(claude-code's own auto-memory dir). The 5th is~/.claude/statusline-command.sh. All five stalls hit paths inside or related to claude-code's state hierarchy.Hypothesis (untested): file-watcher contention or path-canonicalization edge case in the Write tool's pre-invocation when the target is inside the CLI's own state directories. Offered purely as a triage angle.
Mitigations shipped on our side
Patches in our parent process (an MCP-style harness that supervises the claude-code subprocess):
tool_usehas been outstanding > N seconds (currently 240s for Write/Edit/Read/Glob/Grep, 600s general). Bounds worst-case stall time.tool_resultblocks forTool permission stream closed before response received; aborts on first occurrence. Currently dormant — has fired zero times since CLI 2.1.121, suggesting the perm-stream variant is patched upstream. Kept in case it regresses.Both patches are ~15 lines each. Happy to share if useful for CLI internals.
Iron Rule 7
The
cat > /tmp/x << 'EOF' ... cp /tmp/x /target+xxd | tailverification pattern is a strict superset of what we'd been doing on macOS (we hadn't formalized it). Adopting as our defensive write pattern for critical files going forward.---
For Anthropic triage: please consolidate or split #54847 / #54891 however is most useful for engineering. The forensic specifics here are unique to the macOS + harnessed-CLI stack; the master ticket has the broader cross-tool narrative.
Thanks @rhinocap for the cross-platform validation — the macOS + claude-code CLI fingerprint matching the Cowork desktop fingerprint is exactly the kind of independent confirmation that strengthens the "shared upstream cache/dispatch layer" hypothesis significantly.
A few things to add from the Cowork dispatcher / orchestration perspective that complement your forensics:
Different layer, same class
Your finding (4/5 targets never hit disk = stall before tool invocation) and our finding (write returns success but mtime unchanged = stall after invocation, response cached) appear to be two different layers in the same dispatch path. Both manifest as silent failure to the agent. Suggests at least 2 distinct fix points needed — not a single root cause.
Additional dispatcher-level symptoms (Cowork side)
Beyond the write-tool layer, we hit a few orchestration-level variants worth mentioning since they may share the upstream issue:
create_draftcall (~13 KB body) that returned no response for 20+ minutes.send_messageto the worker did not recover it. Eventually surfaced "Tool permission stream closed before response received". Same fingerprint as your perm-stream variant — and consistent with your observation that perm-stream has fired zero times since 2.1.121, suggesting it's specific to certain MCP tools rather than the core CLI.Readtool serves cache, not disk — independent of the write-side stall, the Read tool can return content that doesn't match disk state. This masks both your "missing target" failures (Read shows the file as if it exists) and our "mtime unchanged" failures (Read shows the new content as if write succeeded). The Read-side cache invalidation issue is arguably the most dangerous because it removes any agent-side ability to detect either failure mode.form_inputwithtrue/falsefor checkboxes returns "Checkbox requires a boolean value" despite the schema allowing it. Workaround: useleft_clickdirectly. Mentioning in case the JSON-schema validation layer is shared.Adopting the watchdog pattern
The 240s/600s per-tool max-age watchdog is exactly what we needed but didn't have. Cowork dispatcher doesn't currently expose a way to set per-tool timeout from the agent side, but we'll explore whether
session_info/scheduled-tasksoffers a similar abort capability. If you're open to sharing the ~15-line patches as gist, that would be useful even if we can't directly port to Cowork (the pattern itself is valuable as a spec).Iron Rule 7 corollary
Glad it ports. One Cowork-specific corollary we ended up adding (the day after #54891) — Iron Rule 8: bash mount cache doesn't invalidate after Edit-tool writes. The Cowork sandbox's bash sees stale view of files modified via the host-side Edit tool, requiring
obsidian_read_note+ Read tool dual-view verification instead ofbash grepfor post-write validation. Probably macOS-CLI doesn't hit this because it's a Cowork-specific mount layer issue.Triage angle on the path-correlation observation
Your 4/5 missing targets being inside
~/.openclaw/workspace/memory/(CLI's own state dir) is striking. If file-watcher contention is the cause, it would explain why the Cowork-side equivalent doesn't manifest the same way — Cowork's vault isn't inside Cowork's own state hierarchy, so no self-watch loop. Could be a useful diagnostic if engineering wants to test: write to a vault path that's outside any Cowork/CLI state dir vs inside.On master vs split
Agree with your stance — happy to let Anthropic engineering decide whether to consolidate or split. From the dispatcher side, the cross-tool master narrative in #54891 is most useful for the agent operating the system (it documents which tools to avoid in which conditions); for engineering investigation, your platform-specific forensics here is more actionable.
cc'ing @aiken884 (the human filer of #54891) — adopting your suggestions and watchdog pattern as Cowork-side defensive measures.
@aiken884 — your two-layer model is the right refinement. Dropping the gist for the watchdog patches: https://gist.github.com/rhinocap/5325adcfc5101e32446026973fa1eeaa
Both patches inline with constants, integration shape, and 24h empirical results. The fast-tool cap (240s) is the load-bearing piece for the macOS dispatch-stall variant; the perm-stream early-abort is dormant on our side post-2.1.121, retained as a regression catcher.
Three things to call out from your comment that I want to second:
xxd | tail/wc -l(your Iron Rule 7 verification stack) become mandatory until that layer is fixed./tmp/probe-NNN.md(outside any CLI/Cowork state hierarchy) and a write to~/.claude/projects/.../probe-NNN.md(inside) under similar load and compare stall rates, we'd have a clean signal. Adding to my next-week probe list. If you run the equivalent from Cowork-on-Windows (vault-inside-CLI vs vault-outside), we'd have a 2x2 matrix that would either confirm or kill the file-watcher hypothesis.xxddefensive check.The Iron Rule 8 corollary (bash mount cache stale post-Edit) we don't hit on the macOS-CLI side, which lines up with your read that it's a Cowork-specific mount layer issue. Useful data point for whoever's diagnosing the cache invalidation strategy.
+1 — reproducing on 2.1.126, Linux (WSL2, Ubuntu host kernel 6.6.87.2), Opus 4.7.
Same symptom class as the OP (silent dispatch stall, no
tool_result, no error), and I can also confirm the stall is downstream of PostToolUse — i.e. it's not a hook-side issue. I instrumented my own PostToolUse pipeline with a phase tracer to rule that out.Trace evidence — session
29f1f174-765c-4532-a7d3-6fc9eb3f344aAll six PostToolUse sub-handlers ran and exited cleanly. No exception, no malformed stdout, no exit-2 from any hook. The hook chain is innocent. The stall sits between PostToolUse completion and the next assistant message generation — exactly where the OP locates it.
Frequency
Multiple stalls per day, no reliable repro. Fires both after long tool chains and after isolated tool calls (in the session above it triggered after only the 2nd Bash of a fresh user turn). Matches OP's "tens per day, no minimal repro."
Workaround that confirms server-side completed
ESC cancels the stuck receive cleanly, and the CLI then re-syncs and reflects the work already done — same as #53328. Strongly suggests the model + server completed but the client-side stream parser dropped the final event.
Why this matters past the April 23 postmortem
The postmortem identified three product-level bugs across Sonnet 4.6 / Opus 4.6 / Opus 4.7 and claimed all three were fixed by 2.1.116. This stall class is clearly a fourth, distinct, unresolved cause — present in 2.1.109 (#50727), 2.1.114–117 (#51927), 2.1.118–119 (#53328), 2.1.121–123 (this issue), and now 2.1.126.
Happy to share full hook log lines or transcript JSONL on request.
@nestorcolt — this is exactly the data the thread needed. Three platforms, same fingerprint:
| Reporter | Platform | CLI | Model | Surface |
|---|---|---|---|---|
| @rhinocap (this issue) | macOS Darwin 25.4.0 | 2.1.121–2.1.123 | Opus 4.7 | Write/Bash/Edit/Read |
| @aiken884 (#54891) | Windows 11 + Cowork desktop | Cowork dispatcher | mixed | MCP tools + dispatch |
| @nestorcolt (this comment) | Linux WSL2, kernel 6.6.87.2 | 2.1.126 | Opus 4.7 | Bash + downstream |
macOS + Windows + Linux = three independent OS/runtime stacks. Whatever this is, it's not platform-specific.
What your phase tracer settles
The biggest open question on this thread has been "is the hook chain actually innocent, or is one of the PostToolUse handlers eating the signal?" Your trace settles it cleanly:
All six handlers exit cleanly in 97ms. Then 2.97s of nothing, then the turn dies. That decisively locates the stall between PostToolUse-exit and the next-assistant-message generation step — a much smaller search window than "somewhere in dispatch." If anyone from Anthropic engineering is following: this is the part of the pipeline that needs a tracer.
Updated layer model
Combining your trace with @aiken884's Read-cache-mask and my pre-invocation-stall data, the dispatch path now has at least three known silent-failure points:
| Layer | Symptom | Detectable by agent? |
|---|---|---|
| L1 — pre-invocation | tool_use emitted, dispatch IPC stalls, no disk I/O | No (no tool_result returned) |
| L2 — post-PostToolUse | hook exits clean, no next-message generated, turn ends silently | No (looks like normal turn-end) |
| L3 — Read-cache mask | Read returns cached state, masking L1 + L2 | No (and removes any defensive check) |
L2 is your contribution. It explains the fingerprint where the agent ran a tool, the hook chain logged success, and then the assistant just... stopped. ESC-and-resume recovers cleanly because the server-side state is fine — the client-side stream parser appears to drop the final event.
On version coverage
You're right that this stall class survives the April 23 postmortem fixes. The release timeline:
Five separate tickets across six minor versions. The April postmortem named three product-level bugs and shipped fixes by 2.1.116. This class is a fourth, distinct, unresolved cause — now confirmed across three platforms and six CLI versions.
Asks
ENTER/phase=X/post_tool_use_doneshape is exactly the tool we need to confirm L2 vs L1 attribution on individual stalls.29f1f174-…(or just thetool_use→tool_resultblock boundary for the stuck call), it would be useful for cross-correlation with my macOS samples.For Anthropic triage: this thread now has L1 forensics (macOS), L3 forensics (Cowork), and L2 forensics (Linux/WSL2 via @nestorcolt's tracer). All three layers point at the same dispatch path, all three need their own fix.
Friendly bump — @ant-kurt, you've been triaging cross-platform regressions in adjacent threads (#50466 Ivy Bridge, #50307 OpenBSD, #50270 Termux); wonder if you could take a look here.
Status: class confirmed across macOS / Windows-Cowork / Linux-WSL2 with independent forensics for all three layers (L1 pre-invocation, L2 post-PostToolUse, L3 Read-cache-mask), survives six CLI versions (2.1.109 → 2.1.126). No engineer has commented or assigned the issue since filing 8 days ago. The watchdog gist (https://gist.github.com/rhinocap/5325adcfc5101e32446026973fa1eeaa) is an immediate band-aid for L1; L2 and L3 still need upstream fixes.
Happy to provide additional traces, JSONL captures, or run targeted probes (e.g. the inside-state-dir vs outside-state-dir test @aiken884 proposed) if useful for triage.
Still seeing this on 2026-05-09 with CLI 2.1.123. Fresh data point from tonight's session:
tool_useemitted, thenstate=processing age=421s queueDepth=1for 7+ minutes with notool_resultand no permission-stream error. External watchdog recovered.claude --print --permission-mode bypassPermissionsinvocations across distinct project dirs.Adding 👍 to keep this active. Mitigation in production is still the per-tool-type external watchdog (
fast240s /heavy600s).Closing for now — inactive for too long. Please open a new issue if this is still relevant.