fix: address code review feedback

- Change from absolute timeout to inactivity timeout (reset on each data chunk)
  to avoid truncating large/slow payloads
- Fix race condition: add resolved=true before resolving in catch block
- Fix unreliable readable check: just access the property, don't check value
- Add cleanup() call in catch block for consistency

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-01-21 16:07:19 -08:00
committed by Alex Newman
parent c477ded1b5
commit 5cbe0a38ca
+31 -17
View File
@@ -1,10 +1,12 @@
// Stdin reading utility extracted from hook patterns
// See src/hooks/save-hook.ts for the original pattern
// Timeout for stdin reading - if Claude Code doesn't close stdin within this time,
// Inactivity timeout for stdin reading - if no data arrives within this time,
// we parse whatever data we have. This fixes issue #727 where hooks hang at "1/2 done"
// because stdin.on('end') never fires.
const STDIN_TIMEOUT_MS = 5000;
// Using inactivity timeout (reset on each data chunk) instead of absolute timeout
// to avoid truncating large/slow payloads.
const STDIN_INACTIVITY_TIMEOUT_MS = 5000;
/**
* Check if stdin is available and readable.
@@ -24,11 +26,13 @@ function isStdinAvailable(): boolean {
return false;
}
// Check if we can access basic stdin properties without crashing
// This triggers Bun's lazy initialization
const readable = stdin.readable;
return readable !== false;
} catch (err) {
// Accessing stdin.readable triggers Bun's lazy initialization.
// If we get here without throwing, stdin is available.
// Note: We don't check the value since Node/Bun don't reliably set it to false.
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
stdin.readable;
return true;
} catch {
// Bun crashed trying to access stdin (EINVAL from fstat)
// This is expected when Claude Code doesn't provide valid stdin
return false;
@@ -45,6 +49,7 @@ export async function readJsonFromStdin(): Promise<unknown> {
return new Promise((resolve, reject) => {
let input = '';
let resolved = false;
let timeoutId: ReturnType<typeof setTimeout>;
const cleanup = () => {
try {
@@ -59,6 +64,7 @@ export async function readJsonFromStdin(): Promise<unknown> {
const resolveWithData = () => {
if (resolved) return;
resolved = true;
clearTimeout(timeoutId);
cleanup();
try {
resolve(input.trim() ? JSON.parse(input) : undefined);
@@ -67,25 +73,31 @@ export async function readJsonFromStdin(): Promise<unknown> {
}
};
// Timeout handler - resolve with whatever data we have
// This fixes issue #727 where stdin.on('end') never fires
const timeoutId = setTimeout(() => {
if (!resolved) {
resolveWithData();
}
}, STDIN_TIMEOUT_MS);
// Reset the inactivity timeout - called on each data chunk
const resetTimeout = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
if (!resolved) {
resolveWithData();
}
}, STDIN_INACTIVITY_TIMEOUT_MS);
};
// Start initial timeout
resetTimeout();
try {
process.stdin.on('data', (chunk) => {
input += chunk;
// Reset timeout on each data chunk to avoid truncating large/slow payloads
resetTimeout();
});
process.stdin.on('end', () => {
clearTimeout(timeoutId);
resolveWithData();
});
process.stdin.on('error', (err) => {
process.stdin.on('error', () => {
if (resolved) return;
resolved = true;
clearTimeout(timeoutId);
@@ -94,9 +106,11 @@ export async function readJsonFromStdin(): Promise<unknown> {
// This is more graceful for hook execution
resolve(undefined);
});
} catch (err) {
} catch {
// If attaching listeners fails (Bun stdin issue), resolve with undefined
resolved = true;
clearTimeout(timeoutId);
cleanup();
resolve(undefined);
}
});