fix(hooks): preserve Stop output through lifecycle wrappers (#2493)

Preserve complete Stop-hook stdout through lifecycle wrappers, wait for queued output to flush before exiting, bound child output with a larger explicit buffer, and add end-to-end regressions for large, multibyte, dry-run, and failure cases.
This commit is contained in:
JongHyeok Park
2026-07-26 00:01:57 -07:00
committed by GitHub
parent c714dc5654
commit 28b922dee3
4 changed files with 188 additions and 18 deletions
+19 -10
View File
@@ -46,17 +46,26 @@ function writeStderr(stderr) {
}
/**
* Write stdout fully, then exit. `process.exit()` immediately after
* `process.stdout.write()` drops anything beyond the ~64KB pipe buffer,
* which cut large pass-through payloads mid-JSON and made the harness
* treat the hook as failed (#2222). The write callback fires only after
* the chunk is flushed to the pipe.
* Exit only after stdout and any previously queued stderr have drained.
* `process.exit()` immediately after a stream write drops anything beyond
* the OS pipe buffer, which cut large hook output mid-payload and made the
* harness treat the hook as failed (#2222).
*/
function exitWithStdout(text, exitCode) {
if (typeof text !== 'string' || text.length === 0) {
process.exit(exitCode);
process.exitCode = exitCode;
let pendingWrites = 1;
const exitWhenFlushed = () => {
pendingWrites -= 1;
if (pendingWrites === 0) {
process.exit(exitCode);
}
};
if (typeof text === 'string' && text.length > 0) {
pendingWrites += 1;
process.stdout.write(text, exitWhenFlushed);
}
process.stdout.write(text, () => process.exit(exitCode));
process.stderr.write('', exitWhenFlushed);
}
function resolveHookResult(raw, output) {
@@ -169,8 +178,8 @@ async function main() {
if (isDryRun()) {
const preview = buildDryRunPreview(hookId, relScriptPath, profilesCsv, raw);
process.stderr.write(preview);
process.stdout.write(raw);
process.exit(0);
exitWithStdout(sanitizeEcho(raw), 0);
return;
}
const pluginRoot = getPluginRoot();