fix: Windows --print (pipe) mode hangs when plugin hooks block on stdin#1838
fix: Windows --print (pipe) mode hangs when plugin hooks block on stdin#1838jakeefr wants to merge 1 commit intothedotmack:mainfrom
Conversation
Skip stdin collection for the "start" subcommand which spawns a daemon and never reads stdin. Reduce the stdin safety timeout from 5s to 500ms on Windows — Claude Code delivers hook input in milliseconds when present, and the long timeout causes SessionStart hooks to block for up to 15s total (3 hooks × 5s), producing empty output in --print mode. Fixes thedotmack#1482 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary by CodeRabbit
WalkthroughThe PR modifies stdin collection behavior in the bun-runner to skip waiting for stdin when the Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@plugin/scripts/bun-runner.js`:
- Around line 162-167: The current fixed timeout (stdinTimeoutMs) uses
setTimeout that can fire mid-stream and truncate chunks; change this to an
inactivity (idle) timer: create a timer variable (e.g., idleTimer) instead of
the one-shot setTimeout, start/reset idleTimer on each 'data' event handler, and
clear it on 'end' and 'error' before resolving; when idleTimer finally fires,
perform the same cleanup (process.stdin.removeAllListeners, process.stdin.pause,
resolve(Buffer.concat(chunks) or null)). Ensure the existing symbols
(stdinTimeoutMs, chunks, resolve, and the process.stdin event handlers) are
updated so the timer is reset on incoming data and cleared on stream end/error.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 77746265-94be-4eb8-a5f3-3b74b6b25590
📒 Files selected for processing (2)
plugin/scripts/bun-runner.jstests/bun-runner.test.ts
| const stdinTimeoutMs = IS_WINDOWS ? 500 : 5000; | ||
| setTimeout(() => { | ||
| process.stdin.removeAllListeners(); | ||
| process.stdin.pause(); | ||
| resolve(chunks.length > 0 ? Buffer.concat(chunks) : null); | ||
| }, 5000); | ||
| }, stdinTimeoutMs); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n plugin/scripts/bun-runner.js | sed -n '140,180p'Repository: thedotmack/claude-mem
Length of output: 2097
Use an inactivity timeout instead of an absolute timeout to prevent truncating valid stdin streams.
The current implementation at lines 162–167 sets a one-shot timeout that fires after a fixed duration (500ms on Windows, 5000ms elsewhere) regardless of when data actually arrives. The data handler at line 148 does not reset this timer. On slower pipes delivering valid stdin within those intervals, the timeout will fire and resolve with incomplete chunks, passing truncated JSON to the child process.
Switch to an idle timeout that resets on each incoming data chunk and clears when the stream ends or errors. This ensures the timeout only fires on genuine inactivity while allowing slower but valid streams to complete.
Suggested patch
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 stdinTimeoutMs = IS_WINDOWS ? 500 : 5000;
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);
- });
+ let timeoutId;
+ const cleanup = () => {
+ process.stdin.off('data', onData);
+ process.stdin.off('end', onEnd);
+ process.stdin.off('error', onError);
+ process.stdin.pause();
+ if (timeoutId) clearTimeout(timeoutId);
+ };
+ const finish = (value) => {
+ cleanup();
+ resolve(value);
+ };
+ const armTimeout = () => {
+ if (timeoutId) clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => {
+ finish(chunks.length > 0 ? Buffer.concat(chunks) : null);
+ }, stdinTimeoutMs);
+ };
+ const onData = (chunk) => {
+ chunks.push(chunk);
+ armTimeout();
+ };
+ const onEnd = () => finish(chunks.length > 0 ? Buffer.concat(chunks) : null);
+ const onError = () => finish(null);
+
+ process.stdin.on('data', onData);
+ process.stdin.on('end', onEnd);
+ process.stdin.on('error', onError);
- // Safety: if no data arrives within timeout, proceed without stdin.
- // On Windows, use a shorter timeout (500ms) because the 5s wait causes
- // --print (pipe) mode to hang or produce empty output — Claude Code's hook
- // stdin data arrives in milliseconds when present, and the long timeout
- // blocks SessionStart hooks for up to 15s total (3 hooks × 5s). Fixes `#1482`.
- const stdinTimeoutMs = IS_WINDOWS ? 500 : 5000;
- setTimeout(() => {
- process.stdin.removeAllListeners();
- process.stdin.pause();
- resolve(chunks.length > 0 ? Buffer.concat(chunks) : null);
- }, stdinTimeoutMs);
+ // Safety: proceed without stdin after idle timeout.
+ armTimeout();
});
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@plugin/scripts/bun-runner.js` around lines 162 - 167, The current fixed
timeout (stdinTimeoutMs) uses setTimeout that can fire mid-stream and truncate
chunks; change this to an inactivity (idle) timer: create a timer variable
(e.g., idleTimer) instead of the one-shot setTimeout, start/reset idleTimer on
each 'data' event handler, and clear it on 'end' and 'error' before resolving;
when idleTimer finally fires, perform the same cleanup
(process.stdin.removeAllListeners, process.stdin.pause,
resolve(Buffer.concat(chunks) or null)). Ensure the existing symbols
(stdinTimeoutMs, chunks, resolve, and the process.stdin event handlers) are
updated so the timer is reset on incoming data and cleared on stream end/error.
xkonjin
left a comment
There was a problem hiding this comment.
Code review for PR 1838:
This looks like a solid fix for the Windows pipeline hang issues (#1482).
A couple of observations:
- Shortening the timeout to
500msfor Windows makes sense if data normally arrives instantly. Are there any edge cases (like extreme system load) where Windows might take >500ms to pipe stdin? Given that failing to read stdin just proceeds with empty stdin, it's a safe failure mode, but worth noting. - Skipping stdin for
startis a great optimization to avoid the blocking wait altogether for daemon startup. - The test coverage validates the presence of the logic in the script string (which I assume is how
bun-runner.test.tsis structured, checking the source directly).
Code is clean and directly addresses the described issue.
|
Closed during the April 2026 backlog cleanup. This was verified already-fixed in v12.1.1 HEAD. Please update to the latest build. If you still see the issue on current version, open a fresh ticket with repro. |
Fixes #1482. Fixed pipe mode detection failing on Windows when using claude --print.
I maintain PRISM (https://github.com/jakeefr/prism), a post-session diagnostics tool for Claude Code — CLAUDE.md adherence analysis and session health scoring from the same JSONL files. claude-mem handles session memory, PRISM handles session health — complementary tools.