fix(cli): suppress EPIPE crashes in piped agent environments (#1184)

* fix(cli): suppress EPIPE crashes in piped agent environments

When the CLI runs inside a piped environment (Claude Code, Codex,
Cursor), the reader may close the pipe before we finish writing.
Node treats EPIPE on stdout/stderr as an uncaughtException, crashing
the process with a non-zero exit code.

Add stream-level EPIPE handlers on stdout/stderr at the top of the
entry point (before any output) and make the uncaughtException handler
EPIPE-aware so it exits cleanly (code 0) instead of crash-reporting.

PostHog data: ~10,300 EPIPE errors over 10 days, contributing to the
preview command's 43-59% failure rate in agent environments.

* fix(cli): set commandFailed before EPIPE exit to prevent false success telemetry

EPIPE is a pipe-reader-closed signal, not a successful run. The exit handler
uses 'code === 0 && !commandFailed' to determine success — without setting
commandFailed=true before process.exit(0), every EPIPE exit was recorded as
success:true in telemetry.

Moves the commandFailed declaration to the top of the file so the stream-error
EPIPE handlers (which must run before any writes) can reference it. Also sets
commandFailed=true in the uncaughtException EPIPE path for the same reason.
This commit is contained in:
Miguel Ángel
2026-06-03 22:53:08 -04:00
committed by GitHub
parent 6de6ea5349
commit cabd0616ea
+25 -1
View File
@@ -1,5 +1,26 @@
#!/usr/bin/env node
// ── EPIPE suppression (must run before ANY stdout/stderr write) ────────────
// When the CLI runs inside a piped agent environment (Claude Code, Codex,
// Cursor, etc.), the reader may close the pipe before we finish writing.
// Node treats EPIPE on stdout/stderr as an uncaughtException, which crashes
// the process. This is a normal lifecycle event — suppress it.
//
// commandFailed must be declared here (before the handlers) so the EPIPE
// stream-error path can set it before process.exit(0). The telemetry exit
// handler reads this flag to determine success/failure — an EPIPE exit
// should NOT score as success:true in telemetry.
let commandFailed = false;
for (const stream of [process.stdout, process.stderr]) {
stream.on("error", (err) => {
if ((err as NodeJS.ErrnoException).code === "EPIPE") {
commandFailed = true;
process.exit(0);
}
});
}
// ── Worker entry path bootstrap (must run before any producer/engine load) ──
// The hf#677 worker_threads pools (`pngDecodeBlitWorkerPool`,
// `shaderTransitionWorkerPool`) live in the producer package and try to
@@ -194,7 +215,6 @@ if (!isHelp && !hasJsonFlag && command !== "upgrade") {
}
const commandStart = Date.now();
let commandFailed = false;
// Async flush for normal exit. `beforeExit` re-fires every time the
// event loop drains, and the async `_flush()` itself schedules new
@@ -220,6 +240,10 @@ process.on("exit", (code) => {
});
process.on("uncaughtException", (error) => {
if ((error as NodeJS.ErrnoException).code === "EPIPE") {
commandFailed = true;
process.exit(0);
}
commandFailed = true;
_trackCliError?.({
error_name: error.name,