claude-code - 💡(How to fix) Fix sendRequest throws 'Stream closed' permanently after inputClosed flag flips (no reset path) [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#51851Fetched 2026-04-23 07:43:14
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Participants
Timeline (top)
labeled ×4

Running Claude Code 2.1.117 (native ARM64 macOS binary) under an ACP parent harness (OpenClaw 2026.4.15). After an initially-healthy period, sendRequest with subtype: "can_use_tool" begins throwing Error("Stream closed") synchronously on every subsequent non-allowlisted tool call, for the remainder of the process lifetime. The process continues to respond to notifications and behaves healthy from outside — it's permanently half-dead.

CLAUDE_CODE_STREAM_CLOSE_TIMEOUT=3600000 has no effect.

Error Message

Running Claude Code 2.1.117 (native ARM64 macOS binary) under an ACP parent harness (OpenClaw 2026.4.15). After an initially-healthy period, sendRequest with subtype: "can_use_tool" begins throwing Error("Stream closed") synchronously on every subsequent non-allowlisted tool call, for the remainder of the process lifetime. The process continues to respond to notifications and behaves healthy from outside — it's permanently half-dead. if (this.inputClosed) throw Error("Stream closed"); // ← the throw if (q?.aborted) throw Error("Request aborted"); q.reject(Error("Tool permission stream closed before response received")); 2. An observable protocol-level signal (notification or stream error) when the flag flips, so the parent can decide to respawn.

Root Cause

Running Claude Code 2.1.117 (native ARM64 macOS binary) under an ACP parent harness (OpenClaw 2026.4.15). After an initially-healthy period, sendRequest with subtype: "can_use_tool" begins throwing Error("Stream closed") synchronously on every subsequent non-allowlisted tool call, for the remainder of the process lifetime. The process continues to respond to notifications and behaves healthy from outside — it's permanently half-dead.

CLAUDE_CODE_STREAM_CLOSE_TIMEOUT=3600000 has no effect.

Fix Action

Workaround

Widening the parent's permissions.allow in settings.json to auto-approve all tools bypasses the broken code path (no can_use_tool roundtrip needed). Not a real fix — it just avoids the trigger for locally-configurable Claude Code sessions. Parent-harness-driven children (which govern permissions at the harness layer, not in settings.json) remain exposed.

Code Example

async sendRequest(H, _, q, K = randomUUID()) {
  let O = {type: "control_request", request_id: K, request: H};
  if (this.inputClosed) throw Error("Stream closed");  // ← the throw
  if (q?.aborted) throw Error("Request aborted");
  this.outbound.enqueue(O);
  if (H.subtype === "can_use_tool" && this.onControlRequestSent) { ... }
}

---

this.inputClosed = true;
for (let q of this.pendingRequests.values())
  q.reject(Error("Tool permission stream closed before response received"));
RAW_BUFFERClick to expand / collapse

Summary

Running Claude Code 2.1.117 (native ARM64 macOS binary) under an ACP parent harness (OpenClaw 2026.4.15). After an initially-healthy period, sendRequest with subtype: "can_use_tool" begins throwing Error("Stream closed") synchronously on every subsequent non-allowlisted tool call, for the remainder of the process lifetime. The process continues to respond to notifications and behaves healthy from outside — it's permanently half-dead.

CLAUDE_CODE_STREAM_CLOSE_TIMEOUT=3600000 has no effect.

Evidence

Extracted via strings dump of the installed binary. The relevant control-request sender:

async sendRequest(H, _, q, K = randomUUID()) {
  let O = {type: "control_request", request_id: K, request: H};
  if (this.inputClosed) throw Error("Stream closed");  // ← the throw
  if (q?.aborted) throw Error("Request aborted");
  this.outbound.enqueue(O);
  if (H.subtype === "can_use_tool" && this.onControlRequestSent) { ... }
}

The EOF handler that flips the flag:

this.inputClosed = true;
for (let q of this.pendingRequests.values())
  q.reject(Error("Tool permission stream closed before response received"));

A binary-wide string search turned up no code path that resets inputClosed = false. Once set — possibly on a transient condition — the child is permanently broken for new can_use_tool roundtrips for the rest of its lifetime.

Why the parent can't observe it

The ACP ClientSideConnection.#receive() loop on the parent only closes the connection when the child's stdout reader ends or errors. Silent inputClosed flips on the child side emit no observable signal to the parent — no connection_close, no protocol-level notification. The parent keeps trying, every permission request throws, and the turn stalls forever until manual SIGTERM.

Expected behavior

One of:

  1. A recovery path that resets inputClosed after transient EOF-like conditions.
  2. An observable protocol-level signal (notification or stream error) when the flag flips, so the parent can decide to respawn.
  3. Clear documentation of what conditions flip the flag, so parents can avoid them.

Repro

Intermittent in real workloads. Triggers appear correlated with long-running prompts that hit backpressure on stdin, but we haven't been able to produce a deterministic repro — the bug only fires sporadically across ~dozens of Discord-driven sessions per day.

Workaround

Widening the parent's permissions.allow in settings.json to auto-approve all tools bypasses the broken code path (no can_use_tool roundtrip needed). Not a real fix — it just avoids the trigger for locally-configurable Claude Code sessions. Parent-harness-driven children (which govern permissions at the harness layer, not in settings.json) remain exposed.

Environment

  • Claude Code: 2.1.117 (native ARM64 macOS binary, ~200MB)
  • Platform: macOS 25.4.0 (Darwin)
  • Entrypoint: CLAUDE_CODE_ENTRYPOINT=sdk-ts (structuredIO NDJSON on stdin/stdout)
  • Parent harness: OpenClaw 2026.4.15 via @agentclientprotocol/sdk 1.x

extent analysis

TL;DR

The most likely fix is to implement a recovery path that resets inputClosed after transient EOF-like conditions or add an observable protocol-level signal when the flag flips.

Guidance

  • Investigate the conditions that flip the inputClosed flag and determine if they are transient or permanent to decide on the best recovery approach.
  • Consider adding a protocol-level signal or notification when inputClosed is set to true, allowing the parent to detect and respond to the condition.
  • Review the sendRequest function to ensure it handles the inputClosed flag correctly and consider adding a retry mechanism for transient conditions.
  • Evaluate the effectiveness of the current workaround (widening the parent's permissions.allow in settings.json) and consider implementing a more robust solution.

Example

No code snippet is provided as the issue requires a more in-depth analysis of the codebase and the specific conditions that trigger the inputClosed flag.

Notes

The issue appears to be related to the handling of EOF conditions and the inputClosed flag, but the exact cause and solution may require further investigation and analysis of the codebase.

Recommendation

Apply a workaround by implementing a recovery path that resets inputClosed after transient EOF-like conditions, as this is the most likely fix based on the provided information. This approach will allow the child process to recover from transient conditions and continue functioning correctly.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

One of:

  1. A recovery path that resets inputClosed after transient EOF-like conditions.
  2. An observable protocol-level signal (notification or stream error) when the flag flips, so the parent can decide to respawn.
  3. Clear documentation of what conditions flip the flag, so parents can avoid them.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING