diff --git a/packages/bash/bash-local/src/run.ts b/packages/bash/bash-local/src/run.ts index 4b043e3fe3..5b67bbdc1b 100644 --- a/packages/bash/bash-local/src/run.ts +++ b/packages/bash/bash-local/src/run.ts @@ -308,12 +308,6 @@ export function runBash(spec: SpawnSpec, internals: RunInternals = {}): RunningB detached: true, }) - // A child that exits without reading stdin makes the write error EPIPE — - // swallow it (the command's outcome rides on its exit code/output, not the - // stdin write) so it never crashes the host or rejects `done`. - child.stdin.on('error', () => { /* EPIPE: child closed stdin early; outcome rides on exit. */ }) - child.stdin.end(spec.stdin ?? '') - const stdout = new OutputCollector(spec.maxOutputBytes, 'stdout', spillDir) const stderr = new OutputCollector(spec.maxOutputBytes, 'stderr', spillDir) child.stdout.on('data', (chunk: Buffer) => { stdout.push(chunk) }) @@ -347,6 +341,20 @@ export function runBash(spec: SpawnSpec, internals: RunInternals = {}): RunningB } spec.signal?.addEventListener('abort', onAbort, { once: true }) + // Write stdin and close it. This handler must exist: an unhandled 'error' on + // the stream would throw and crash the host. We swallow the error rather than + // reject `done`, and that is correct for ANY stdin-write error, not just the + // common one — the stdin write is BEST-EFFORT, while the command's authoritative + // outcome is its exit code + captured output, which the `close` handler reports + // regardless of whether the write landed. The expected case is EPIPE (the child + // exited without reading, so closing our end of a still-full pipe fails); a rare + // non-EPIPE pipe fault means the command ran with incomplete stdin, and it + // surfaces that itself through its own exit/output (e.g. a hook that gets + // truncated JSON errors out) — rejecting here would instead discard that real + // output and turn it into an opaque infrastructure error, which is worse. + child.stdin.on('error', () => { /* stdin write is best-effort; outcome rides on exit/output. */ }) + child.stdin.end(spec.stdin ?? '') + const done = new Promise((resolve, reject) => { child.on('error', (error) => { // Spawn-level failure (ENOENT cwd, EACCES, …): no close event with