From b2ab65d0fb124bdc1ab22eb67c5b3b5c0f0b498c Mon Sep 17 00:00:00 2001 From: haelyra <49814733+haelyra@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:35:23 -0400 Subject: [PATCH 1/2] fix(hooks): classify platform-dependent raw prefixes --- scripts/hooks/plugin-hook-bootstrap.js | 16 +++++------ .../plugin-hook-bootstrap-no-echo.test.js | 28 ++++++++++--------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/scripts/hooks/plugin-hook-bootstrap.js b/scripts/hooks/plugin-hook-bootstrap.js index 627afeeca..8d573ffed 100644 --- a/scripts/hooks/plugin-hook-bootstrap.js +++ b/scripts/hooks/plugin-hook-bootstrap.js @@ -7,7 +7,6 @@ const { spawnSync } = require('child_process'); const { ensureAgentDataHomeEnv } = require('../lib/agent-data-home'); const SHELL_PROBE_TIMEOUT_MS = 2000; -const STDOUT_PIPE_CAP_BYTES = 64 * 1024; function readStdinRaw() { try { @@ -37,9 +36,8 @@ function isRawPassthrough(raw, stdout) { const stdoutBytes = toBuffer(stdout); if (rawBytes.length === 0 || stdoutBytes.length === 0) return false; return ( - stdoutBytes.equals(rawBytes) || - (stdoutBytes.length === STDOUT_PIPE_CAP_BYTES && - rawBytes.subarray(0, stdoutBytes.length).equals(stdoutBytes)) + stdoutBytes.length <= rawBytes.length && + rawBytes.subarray(0, stdoutBytes.length).equals(stdoutBytes) ); } @@ -58,11 +56,11 @@ function passthrough(result) { // instead; the harness falls back to the tool_use's original result, the // same path #2240 established for bash-hook-dispatcher. // - // IMPORTANT: a strict `stdout === raw` check misses the common case where - // child processes' synchronous `process.stdout.write()` writes hit the - // ~64 KB Node.js pipe buffer and get truncated -- stdout is then exactly - // 65536 bytes and a strict prefix of raw. So we also detect that - // truncation sentinel. + // IMPORTANT: a strict `stdout === raw` check misses child processes whose + // synchronous `process.stdout.write()` is truncated before exit. Pipe + // capacity varies by platform and Node version (observed at 8, 16, and + // 64 KiB), so classify any non-empty byte-exact prefix of the raw hook + // event as passthrough instead of assuming one buffer size. const raw = result?.comparisonInput; const looksLikePassthrough = isRawPassthrough(raw, stdout); if (looksLikePassthrough) { diff --git a/tests/hooks/plugin-hook-bootstrap-no-echo.test.js b/tests/hooks/plugin-hook-bootstrap-no-echo.test.js index ed94df763..3d9f1cb79 100644 --- a/tests/hooks/plugin-hook-bootstrap-no-echo.test.js +++ b/tests/hooks/plugin-hook-bootstrap-no-echo.test.js @@ -242,7 +242,7 @@ if ( else failed++; if ( - test('64 KiB passthrough sentinel is measured in UTF-8 bytes', () => { + test('truncated UTF-8 prefix is classified by bytes', () => { const fixturePath = path.join(FIXTURE_DIR, 'multibyte-prefix-fixture.js'); fs.writeFileSync( fixturePath, @@ -255,7 +255,7 @@ if ( }); assert.strictEqual(Buffer.byteLength(payload.slice(0, 32768), 'utf8'), 64 * 1024); assert.strictEqual(result.status, 0, result.stderr); - assert.strictEqual(result.stdout, '', 'a 64 KiB UTF-8 prefix of raw input must be suppressed'); + assert.strictEqual(result.stdout, '', 'a UTF-8 byte prefix of raw input must be suppressed'); assert.match(result.stderr, /returned raw input as stdout/); } finally { fs.unlinkSync(fixturePath); @@ -266,18 +266,20 @@ if ( else failed++; if ( - test('byte boundary does not misclassify 64K multibyte characters', () => { - const byteBoundaryPrefix = 'é'.repeat(32768); - const characterBoundaryPrefix = 'é'.repeat(65536); + test('classifies platform-dependent pipe prefixes without accepting mismatches', () => { + const raw = Buffer.from(`${'a'.repeat(64 * 1024)}tail`, 'utf8'); - assert.strictEqual(Buffer.byteLength(byteBoundaryPrefix, 'utf8'), 64 * 1024); - assert.strictEqual(Buffer.byteLength(characterBoundaryPrefix, 'utf8'), 128 * 1024); - assert.strictEqual(isRawPassthrough(`${byteBoundaryPrefix}tail`, byteBoundaryPrefix), true); - assert.strictEqual( - isRawPassthrough(`${characterBoundaryPrefix}tail`, characterBoundaryPrefix), - false, - '64K JavaScript characters must not be treated as a 64 KiB byte boundary' - ); + for (const prefixLength of [8 * 1024, 16 * 1024, 64 * 1024]) { + assert.strictEqual( + isRawPassthrough(raw, raw.subarray(0, prefixLength)), + true, + `${prefixLength}-byte raw prefix must be classified as passthrough` + ); + } + + const mismatchedPrefix = Buffer.from(raw.subarray(0, 8 * 1024)); + mismatchedPrefix[mismatchedPrefix.length - 1] ^= 1; + assert.strictEqual(isRawPassthrough(raw, mismatchedPrefix), false); }) ) passed++; From 303f50c57ead0d935509b35a9b0209cf4f829a7b Mon Sep 17 00:00:00 2001 From: haelyra <49814733+haelyra@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:43:28 -0400 Subject: [PATCH 2/2] test(hooks): construct mismatch prefix immutably --- tests/hooks/plugin-hook-bootstrap-no-echo.test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/hooks/plugin-hook-bootstrap-no-echo.test.js b/tests/hooks/plugin-hook-bootstrap-no-echo.test.js index 3d9f1cb79..e896ec48b 100644 --- a/tests/hooks/plugin-hook-bootstrap-no-echo.test.js +++ b/tests/hooks/plugin-hook-bootstrap-no-echo.test.js @@ -277,8 +277,11 @@ if ( ); } - const mismatchedPrefix = Buffer.from(raw.subarray(0, 8 * 1024)); - mismatchedPrefix[mismatchedPrefix.length - 1] ^= 1; + const mismatchLength = 8 * 1024; + const mismatchedPrefix = Buffer.concat([ + raw.subarray(0, mismatchLength - 1), + Buffer.from([raw[mismatchLength - 1] ^ 1]) + ]); assert.strictEqual(isRawPassthrough(raw, mismatchedPrefix), false); }) )