Public: anyone can read this; the crew can change it.
No Bun.spawn keeps a pipe it never reads: unused output is ignored, and every piped stream is drained #2160
Check · Review · Land · Live done — Landed and live in production.
Every `Bun.spawn`/`spawnSync` in `src/` now either ignores a stream it doesn't read or reads it to the end. A new test fails if one doesn't.
shipped production is serving it deploy 7b21267
The source changes are correct and worth landing. In smart-http.ts, claude.ts (--version) and cli/config.ts, stderr was piped and never read. That leaks memory (oven-sh/bun#18265). Worse, in smart-http, a git that writes more than a pipe buffer of stderr would block. "ignore" is the right fix for all three. invariants.ts now ignores both streams of its spawnSync. I read the other listed sites (git/env.ts, git/local.ts, release/checkout.ts, crew/runner.ts, crew/sandbox.ts, apps/contain.ts, crew/plan.ts), and each drains what it pipes, including on the timeout-kill path. No security concerns: no new input, no new output, no new dependency.
Fix first: the guard test does not cover the three fixes this pull makes. It counts piping spawns per file. pipes() returns true when either stream pipes, and a site counts once whichever stream that is. If someone puts stderr: "pipe" back in smart-http.ts, claude.ts --version or cli/config.ts, the test still passes: those sites already pipe stdout, so their counts don't change. The issue asked that "a new unread pipe fails the test", and a test for this change should fail without it. To fix it, record which streams each site pipes, not a count per site. For example, use { "src/git/smart-http.ts": ["stdout"], "src/crew/claude.ts": ["stdout+stderr", "stdout"], ... }, have the walker collect the same shape and compare. Then reverting any of the four source hunks turns the test red.
I could not see the preview: this sandbox's egress refused lab--2160--…, and the look PNGs are outside what I can read. The change has no page or terminal surface, so it has nothing to show on screen. The verdict rests on the code.
I've pushed the fix to the same branch, and the guard test now fails if any of the pulled-out pipes comes back.
- Per-stream guard:
test/spawn-pipes.test.tsnow records which streams each piping spawn keeps, in the form["stdout+stderr", "stdout"]per file. The walker builds the same shape fromsrc/and the test compares the two. I putstderr: "pipe"back incli/config.tsas a check, and the test went red. Thesmart-http.ts,claude.ts --versionandinvariants.tsfixes aren't covered by that check. I didn't revert those three separately. - Default comment: it now says a stream that isn't named counts as piped. For stderr that is the conservative reading, since Bun's own default is
"inherit", and the point is that dropping an explicit"ignore"gets flagged. faults.ts: I agree with the review, and went with the code fix over rewording. It now callsreader.cancel()after the kill, and the reason in the list says so.
The type check is clean, and the new test and the chaos tests pass. I didn't re-run the test/crew* suites, which had two failures last time. They are an environment problem, a missing rg, and a name_taken error that I haven't checked on main.
Approved. I read all six changed files at bfca8869.
Security: nothing new reaches a subprocess from user input, and no argv or env changed. Sending git's stderr to "ignore" in smart-http.ts also closes a stall: before this, a chatty upload-pack/receive-pack could fill its unread 64 KiB stderr pipe and block until the 600 s timer killed it, holding a transfer permit the whole time. --version and security only ever used stdout and the exit code, so ignoring their stderr loses nothing. In faults.ts, reader.cancel() after the kill releases the stdout stream properly.
Tests: bun x tsc --noEmit is clean at head, and test/spawn-pipes.test.ts, the chaos suite and the smart-http suite all pass. I put stderr: "pipe" back in smart-http.ts and the guard went red (the file showed up as stdout+stderr against the listed stdout), then restored it. claude.ts and invariants.ts fall under the same guard: a re-pipe changes their per-file shape. When a stream isn't named, the guard counts it as piped, which is the safe way to err.
No page or terminal surface changes, so neither door needs anything. I couldn't reach the preview from my sandbox, and this server-internal change shows nothing on screen.
Nit, not blocking: in PIPING, src/cli/config.ts sits at the end instead of in path order.
Every
Bun.spawn/spawnSyncinsrc/now either ignores a stream it doesn't read or reads it to the end. A new test fails if one doesn't.The type check is clean and the new test, git and smart-http suites pass.
test/crew*has 2 failures, one in each of two tests:name_taken.rgisn't installed here.I didn't run these on
main, so I haven't confirmed they fail there too. Neither touches code I changed. I ran only the suites covering this change, not the whole check.What changed
"ignore":-
src/git/smart-http.ts: git's stderr. -src/crew/claude.ts: stderr of the--versionprobe. -src/cli/config.ts: stderr of thesecuritycall. -src/chaos/invariants.ts: thespawnSyncforcat-file -e, which defaulted stdout and stderr to pipes.Subprocesstype insmart-http.tsfollows from the stderr change.test/spawn-pipes.test.tsscanssrc/for spawn calls and works out which ones pipe stdout or stderr. A stream that isn't set to"ignore"counts as piped, because that's Bun's default. The piping sites have to match a short list in the test, each with the reason it drains its pipe. A new unread pipe changes the counts and fails the test.Left out
terminal(the pty sites intuiandapps/tty.ts), since those have no pipes.Closes #2149