From ddc25372c13f3bdd2263119290f859cda0f4baa6 Mon Sep 17 00:00:00 2001 From: yczc3999 Date: Mon, 16 Feb 2026 13:30:42 +0800 Subject: [PATCH] fix(linux): buffer stdin in Node.js before passing to Bun (#646) (#977) On Linux, Bun's libuv calls fstat() on inherited pipe file descriptors and crashes with EINVAL when the pipe originates from Claude Code's hook system. This causes all PostToolUse hooks to fail silently, preventing observations from being recorded. The fix reads stdin entirely in the Node.js parent process (bun-runner.js) before spawning Bun, then writes the buffered data to a fresh pipe created by Node's child_process.spawn(). Bun receives a standard pipe that it can fstat() without errors. Changes: - Add collectStdin() to buffer piped input in Node.js with 5s safety timeout - Change stdio from 'inherit' to ['pipe'|'ignore', 'inherit', 'inherit'] - Write buffered stdin to child.stdin then close for proper EOF signaling - Handle edge cases: TTY stdin, no stdin, read errors Fixes #646 Co-authored-by: yczc3999 Co-authored-by: Claude Opus 4.6 --- plugin/scripts/bun-runner.js | 42 +++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/plugin/scripts/bun-runner.js b/plugin/scripts/bun-runner.js index 80e8f44f..414fc332 100644 --- a/plugin/scripts/bun-runner.js +++ b/plugin/scripts/bun-runner.js @@ -70,16 +70,56 @@ if (!bunPath) { process.exit(1); } +// Fix #646: Buffer stdin in Node.js before passing to Bun. +// On Linux, Bun's libuv calls fstat() on inherited pipe fds and crashes with +// EINVAL when the pipe comes from Claude Code's hook system. By reading stdin +// in Node.js first and writing it to a fresh pipe, Bun receives a normal pipe +// that it can fstat() without errors. +function collectStdin() { + return new Promise((resolve) => { + // If stdin is a TTY (interactive), there's no piped data to collect + if (process.stdin.isTTY) { + resolve(null); + return; + } + + const chunks = []; + process.stdin.on('data', (chunk) => chunks.push(chunk)); + process.stdin.on('end', () => { + resolve(chunks.length > 0 ? Buffer.concat(chunks) : null); + }); + process.stdin.on('error', () => { + // stdin may not be readable (e.g. already closed), treat as no data + resolve(null); + }); + + // Safety: if no data arrives within 5s, proceed without stdin + setTimeout(() => { + process.stdin.removeAllListeners(); + process.stdin.pause(); + resolve(chunks.length > 0 ? Buffer.concat(chunks) : null); + }, 5000); + }); +} + +const stdinData = await collectStdin(); + // Spawn Bun with the provided script and args // Use spawn (not spawnSync) to properly handle stdio // Note: Don't use shell mode on Windows - it breaks paths with spaces in usernames // Use windowsHide to prevent a visible console window from spawning on Windows const child = spawn(bunPath, args, { - stdio: 'inherit', + stdio: [stdinData ? 'pipe' : 'ignore', 'inherit', 'inherit'], windowsHide: true, env: process.env }); +// Write buffered stdin to child's pipe, then close it so the child sees EOF +if (stdinData && child.stdin) { + child.stdin.write(stdinData); + child.stdin.end(); +} + child.on('error', (err) => { console.error(`Failed to start Bun: ${err.message}`); process.exit(1);