[BUG] macOS sandbox proxy truncates response bodies and resets the connection when the server closes it (`Connection: close`)

Status Open
Reported on v2.1.286
Maintainer reply None cached
Activity 0 comments · opened Oct 5, 2026

Summary

With the Bash sandbox enabled on macOS, HTTPS responses that end with the server closing the connection (for example, because the request sent Connection: close) often arrive truncated, then the client socket gets an ECONNRESET. The same requests succeed every time outside the sandbox, both directly and through a minimal CONNECT proxy.

HTTP clients that miss that late socket error never see the end of the body. The process then waits forever, or exits with code 0 once its event loop is empty. One real case is next build: Next.js 14 (node-fetch v2) and 15 (https.request behind a per-request proxy agent) download Google Fonts this way. In my runs the build hung for 30+ minutes with the webpack build worker on, or, with the worker off, exited 0 having built nothing.

Environment

  • Claude Code 2.1.286, non-interactive (claude -p), sandbox enabled through --settings
  • macOS 27.0.1 (26A434), arm64
  • Node.js v26.10.0, NODE_USE_ENV_PROXY=1, so node:https uses the sandbox's HTTPS_PROXY

Reproduction

settings.json:

{"sandbox": {"enabled": true, "failIfUnavailable": true, "allowUnsandboxedCommands": false,
             "autoAllowBashIfSandboxed": true, "network": {"allowedDomains": ["fonts.gstatic.com"]}}}

repro.cjs downloads 7 small files in parallel and waits for each response's end:

const https = require("node:https");
const mode = process.argv[2] || "https";            // "https" | "close" | "fetch"
const urls = [
  "https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2",
  "https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2",
  "https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2",
  "https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2",
  "https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2",
  "https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2",
  "https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2",
];
const get = (u) => mode === "fetch"
  ? fetch(u).then((r) => r.arrayBuffer()).then((b) => b.byteLength)
  : new Promise((resolve, reject) => {
      const opts = mode === "close" ? { headers: { Connection: "close" } } : {};
      https.get(u, opts, (res) => {
        let n = 0;
        const expected = Number(res.headers["content-length"]);
        res.on("data", (c) => (n += c.length));
        res.on("end", () => resolve(n));
        const fail = (e) => { e.detail = `${n}/${expected} bytes`; reject(e); };
        res.on("error", fail);
        res.req.on("error", fail);
      }).on("error", reject);
    });
let done = false;
Promise.all(urls.map(get)).then(
  (s) => { done = true; console.log("ok", s.reduce((a, b) => a + b, 0)); },
  (e) => { done = true; console.log("error", e.code || e.message, e.detail || "before response"); });
process.on("exit", () => { if (!done) console.log("STALLED"); });

Run it 10 times per mode, with Bash inside the sandbox:

claude -p "Run exactly: bash run.sh" --tools Bash --settings settings.json --permission-mode acceptEdits --permission-prompts none

Results (10 runs each)

| Where | https (keep-alive) | close (Connection: close) | built-in fetch |
|---|---|---|---|
| Outside the sandbox, direct | 10/10 ok | 6/6 ok | 10/10 ok |
| Outside the sandbox, through a minimal Node CONNECT proxy | — | 6/6 ok | — |
| Inside the Claude Code sandbox | 10/10 ok | 3/10 ok, 7/10 ECONNRESET mid-body | 10/10 ok |

The failing runs had already received part of the body when the reset arrived, for example 6054/10280, 7432/11272 and 8810/11272 bytes. So the client does get some bytes, and then the connection is reset instead of the rest of the data and a normal close (FIN).

Expected

When the upstream server closes the connection, the sandbox proxy should forward all of the remaining response bytes and then close the client side normally. It should not reset it.

Impact

  • Silent data loss for any tool that sends Connection: close. That includes node-fetch v2, which does so by default.
  • Hangs in common builds. next build (Next.js 14 and 15) stalls while downloading next/font/google fonts. Agents running in the sandbox cannot kill the stuck processes (the process list is not visible), so the session waits until an outer timeout.

Workaround

Use a client that keeps the connection alive, such as Node's built-in fetch (undici), which succeeded 10/10. For Next.js, I preload a module that swaps only next/font's downloader for one built on the built-in fetch. With that, the same project builds in about 10 s inside the sandbox (Next 14: 8/8, Next 15: 3/3; before the workaround Next 15 failed 0/3).

View original on GitHub ↗