Skip to content

fix: drain stdio then hard-exit - piped JSON truncated at 64KiB - #63

Open
thossullivan wants to merge 1 commit into
mainfrom
fix/piped-stdout-truncation
Open

fix: drain stdio then hard-exit - piped JSON truncated at 64KiB#63
thossullivan wants to merge 1 commit into
mainfrom
fix/piped-stdout-truncation

Conversation

@thossullivan

Copy link
Copy Markdown
Owner

check.mjs called process.exit() straight after console.log(JSON.stringify(...)), and Node discards unflushed pipe writes on exit — so any --json output past the 64KiB pipe buffer was silently truncated. check <large-repo> --json | jq got broken JSON; file redirection was unaffected, which is why CI logs never showed it. Found by the cross-repo UAT sweep (three local repos produce >64KiB of findings).

The fix, shaped by three adversarial review rounds:

  1. First cut (process.exitCode + return) was rejected: it waits on the whole event loop, so a stray timer hangs the CLI and a beforeExit hook can rewrite the exit code.
  2. Second cut (drain then hard-exit) was rejected: the drain discarded stream errors — EPIPE truncation exited 0, and a dead stderr plus a usage error crashed with 1 instead of 2.
  3. Final: main() returns the code; the top level drains both stdio streams through an error-capturing zero-byte write (temporary error listener, always settles, no unhandled-error race), then hard process.exit()s.

Exit codes: 0/1/2 unchanged. New exit 3 (documented in help) when output fails on a clean run for any reason other than EPIPE; EPIPE stays 0 — the consumer closed the pipe deliberately, and the pipeline's exit code is the consumer's anyway.

Tests: regressions for piped >64KiB stdout and stderr, setInterval preload (must exit, not hang), beforeExit preload (must not alter the code), early-closed stdout, ended stderr + usage error, callback-level EBADF. Also converts the two badge tests from live feed dates — which rotted when claude-opus-4-1 retired on 2026-08-05 — to synthetic date-stable fixtures.

Implemented by Codex (gpt-5.6-luna) under adversarial review, run until clean. Verified locally: full suite green; a 456KB / 807-finding real-repo check now parses whole through a pipe.

🤖 Generated with Claude Code

check.mjs called process.exit() straight after console.log(JSON.stringify(...)),
and Node discards unflushed pipe writes on exit, so any --json output past the
64KiB pipe buffer was silently truncated (repro: check on a large repo | jq).
File redirection was unaffected, which is why CI logs never showed it.

The CLI body now runs in main() returning the exit code; top level drains
stdout and stderr through an error-capturing zero-byte write, then hard
process.exit()s. Three review rounds shaped this: bare process.exitCode waited
on the whole event loop (stray timer hangs the CLI, beforeExit rewrites the
code), and a naive drain discarded stream errors (EPIPE truncation exited 0;
a dead stderr plus a usage error crashed 1 instead of 2).

Exit codes: 0/1/2 unchanged; new exit 3 when output fails on a clean run for
any reason other than EPIPE, which stays 0 (consumer closed the pipe).

Regression tests cover piped >64KiB stdout and stderr, setInterval preload,
beforeExit preload, early-closed stdout, ended stderr with a usage error, and
callback-level EBADF. Also converts the two badge tests from live feed dates,
which rotted when claude-opus-4-1 retired on 2026-08-05, to synthetic fixtures.

Implemented by Codex (gpt-5.6-luna) over three review rounds; verified locally.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant