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 <zxfgds@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user