mirror of
https://github.com/thedotmack/claude-mem
synced 2026-04-25 17:15:04 +02:00
Systematic cleanup of every error handling anti-pattern detected by the automated scanner. 289 issues fixed via code changes, 12 approved with specific technical justifications. Changes across 90 files: - GENERIC_CATCH (141): Added instanceof Error type discrimination - LARGE_TRY_BLOCK (82): Extracted helper methods to narrow try scope to ≤10 lines - NO_LOGGING_IN_CATCH (65): Added logger/console calls for error visibility - CATCH_AND_CONTINUE_CRITICAL_PATH (10): Added throw/return or approved overrides - ERROR_STRING_MATCHING (2): Approved with rationale (no typed error classes) - ERROR_MESSAGE_GUESSING (1): Replaced chained .includes() with documented pattern array - PROMISE_CATCH_NO_LOGGING (1): Added logging to .catch() handler Also fixes a detector bug where nested try/catch inside a catch block corrupted brace-depth tracking, causing false positives. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
188 lines
5.9 KiB
TypeScript
188 lines
5.9 KiB
TypeScript
// Stdin reading utility for Claude Code hooks
|
|
//
|
|
// Problem: Claude Code doesn't close stdin after writing hook input,
|
|
// so stdin.on('end') never fires and hooks hang indefinitely (#727).
|
|
//
|
|
// Solution: JSON is self-delimiting. We detect complete JSON by attempting
|
|
// to parse after each chunk. Once we have valid JSON, we resolve immediately
|
|
// without waiting for EOF. This is the proper fix, not a timeout workaround.
|
|
|
|
import { logger } from '../utils/logger.js';
|
|
|
|
/**
|
|
* Check if stdin is available and readable.
|
|
*
|
|
* Bun has a bug where accessing process.stdin can crash with EINVAL
|
|
* if Claude Code doesn't provide a valid stdin file descriptor (#646).
|
|
* This function safely checks if stdin is usable.
|
|
*/
|
|
function isStdinAvailable(): boolean {
|
|
try {
|
|
const stdin = process.stdin;
|
|
|
|
// If stdin is a TTY, we're running interactively (not from Claude Code hook)
|
|
if (stdin.isTTY) {
|
|
return false;
|
|
}
|
|
|
|
// 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 (error) {
|
|
// Bun crashed trying to access stdin (EINVAL from fstat)
|
|
// This is expected when Claude Code doesn't provide valid stdin
|
|
logger.debug('HOOK', 'stdin not available (expected for some runtimes)', { error: error instanceof Error ? error.message : String(error) });
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Try to parse the accumulated input as JSON.
|
|
* Returns the parsed value if successful, undefined if incomplete/invalid.
|
|
*/
|
|
function tryParseJson(input: string): { success: true; value: unknown } | { success: false } {
|
|
const trimmed = input.trim();
|
|
if (!trimmed) {
|
|
return { success: false };
|
|
}
|
|
|
|
try {
|
|
const value = JSON.parse(trimmed);
|
|
return { success: true, value };
|
|
} catch (error) {
|
|
// JSON is incomplete or invalid — expected during incremental parsing
|
|
logger.debug('HOOK', 'JSON parse attempt incomplete', { error: error instanceof Error ? error.message : String(error) });
|
|
return { success: false };
|
|
}
|
|
}
|
|
|
|
// Safety timeout - only kicks in if JSON never completes (malformed input).
|
|
// This should rarely/never be hit in normal operation since we detect complete JSON.
|
|
const SAFETY_TIMEOUT_MS = 30000;
|
|
|
|
// Short delay after last data chunk to try parsing
|
|
// This handles the case where JSON arrives in multiple chunks
|
|
const PARSE_DELAY_MS = 50;
|
|
|
|
export async function readJsonFromStdin(): Promise<unknown> {
|
|
// First, check if stdin is even available
|
|
// This catches the Bun EINVAL crash from issue #646
|
|
if (!isStdinAvailable()) {
|
|
return undefined;
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let input = '';
|
|
let resolved = false;
|
|
let parseDelayId: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
const cleanup = () => {
|
|
try {
|
|
process.stdin.removeAllListeners('data');
|
|
process.stdin.removeAllListeners('end');
|
|
process.stdin.removeAllListeners('error');
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
};
|
|
|
|
const resolveWith = (value: unknown) => {
|
|
if (resolved) return;
|
|
resolved = true;
|
|
if (parseDelayId) clearTimeout(parseDelayId);
|
|
clearTimeout(safetyTimeoutId);
|
|
cleanup();
|
|
resolve(value);
|
|
};
|
|
|
|
const rejectWith = (error: Error) => {
|
|
if (resolved) return;
|
|
resolved = true;
|
|
if (parseDelayId) clearTimeout(parseDelayId);
|
|
clearTimeout(safetyTimeoutId);
|
|
cleanup();
|
|
reject(error);
|
|
};
|
|
|
|
const tryResolveWithJson = () => {
|
|
const result = tryParseJson(input);
|
|
if (result.success) {
|
|
resolveWith(result.value);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Safety timeout - fallback if JSON never completes
|
|
const safetyTimeoutId = setTimeout(() => {
|
|
if (!resolved) {
|
|
// Try one final parse attempt
|
|
if (!tryResolveWithJson()) {
|
|
// If we have data but it's not valid JSON, that's an error
|
|
if (input.trim()) {
|
|
rejectWith(new Error(`Incomplete JSON after ${SAFETY_TIMEOUT_MS}ms: ${input.slice(0, 100)}...`));
|
|
} else {
|
|
// No data received - resolve with undefined
|
|
resolveWith(undefined);
|
|
}
|
|
}
|
|
}
|
|
}, SAFETY_TIMEOUT_MS);
|
|
|
|
const onData = (chunk: Buffer | string) => {
|
|
input += chunk;
|
|
|
|
// Clear any pending parse delay
|
|
if (parseDelayId) {
|
|
clearTimeout(parseDelayId);
|
|
parseDelayId = null;
|
|
}
|
|
|
|
// Try to parse immediately - if JSON is complete, resolve now
|
|
if (tryResolveWithJson()) {
|
|
return;
|
|
}
|
|
|
|
// If immediate parse failed, set a short delay and try again
|
|
// This handles multi-chunk delivery where the last chunk completes the JSON
|
|
parseDelayId = setTimeout(() => {
|
|
tryResolveWithJson();
|
|
}, PARSE_DELAY_MS);
|
|
};
|
|
|
|
const onEnd = () => {
|
|
// stdin closed - parse whatever we have
|
|
if (!resolved) {
|
|
if (!tryResolveWithJson()) {
|
|
// Empty or invalid - resolve with undefined
|
|
resolveWith(input.trim() ? undefined : undefined);
|
|
}
|
|
}
|
|
};
|
|
|
|
const onError = () => {
|
|
if (!resolved) {
|
|
// Don't reject on stdin errors - just return undefined
|
|
// This is more graceful for hook execution
|
|
resolveWith(undefined);
|
|
}
|
|
};
|
|
|
|
try {
|
|
process.stdin.on('data', onData);
|
|
process.stdin.on('end', onEnd);
|
|
process.stdin.on('error', onError);
|
|
} catch (error) {
|
|
// If attaching listeners fails (Bun stdin issue), resolve with undefined
|
|
logger.debug('HOOK', 'Failed to attach stdin listeners', { error: error instanceof Error ? error.message : String(error) });
|
|
resolved = true;
|
|
clearTimeout(safetyTimeoutId);
|
|
cleanup();
|
|
resolve(undefined);
|
|
}
|
|
});
|
|
}
|