Skip to content

fix: Windows --print (pipe) mode hangs when plugin hooks block on stdin#1838

Closed
jakeefr wants to merge 1 commit intothedotmack:mainfrom
jakeefr:fix/windows-print-mode-pipe-detection
Closed

fix: Windows --print (pipe) mode hangs when plugin hooks block on stdin#1838
jakeefr wants to merge 1 commit intothedotmack:mainfrom
jakeefr:fix/windows-print-mode-pipe-detection

Conversation

@jakeefr
Copy link
Copy Markdown

@jakeefr jakeefr commented Apr 15, 2026

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.

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>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 15, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Fixed stdin handling behavior on Windows with optimized timeout values.
    • Improved stdin collection logic to properly handle the start subcommand.
  • Tests

    • Added test coverage for Windows stdin handling and command argument validation.

Walkthrough

The PR modifies stdin collection behavior in the bun-runner to skip waiting for stdin when the start subcommand is present in arguments, and introduces a platform-specific timeout (500ms on Windows, 5000ms otherwise) for stdin collection to resolve the pipe mode issue.

Changes

Cohort / File(s) Summary
Stdin Collection & Timeout Logic
plugin/scripts/bun-runner.js
Modified collectStdin() to compute platform-specific timeout (stdinTimeoutMs), and adjusted main control flow to conditionally skip stdin collection when start subcommand is not present in args, preventing stdin-wait behavior in that mode.
Test Coverage
tests/bun-runner.test.ts
Added new test suite verifying (1) stdin collection is excluded when start subcommand is in args and (2) Windows-specific timeout of 500ms is applied (5000ms otherwise).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • thedotmack

Poem

🐰 On Windows where pipes were slow,
The bun-runner waited, oh so low,
Now fifty millimeters is the call,
And start mode skips the wait—no stall! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing Windows pipe mode hang when plugin hooks block on stdin, which directly addresses the core issue being resolved.
Description check ✅ Passed The description relates to the changeset by referencing issue #1482 and providing context about pipe mode detection failing on Windows, which is what the code changes address.
Linked Issues check ✅ Passed The code changes directly address issue #1482 by implementing both key requirements: (1) skipping stdin collection for the 'start' subcommand to avoid blocking, and (2) reducing the Windows stdin timeout from 5000ms to 500ms to prevent prolonged blocking.
Out of Scope Changes check ✅ Passed All changes are within scope: the bun-runner.js modifications implement the required Windows timeout and stdin-skip logic, while the test additions verify these specific behaviors without introducing unrelated functionality.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7d5d4c5 and c9b425a.

📒 Files selected for processing (2)
  • plugin/scripts/bun-runner.js
  • tests/bun-runner.test.ts

Comment on lines +162 to +167
const stdinTimeoutMs = IS_WINDOWS ? 500 : 5000;
setTimeout(() => {
process.stdin.removeAllListeners();
process.stdin.pause();
resolve(chunks.length > 0 ? Buffer.concat(chunks) : null);
}, 5000);
}, stdinTimeoutMs);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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.

Copy link
Copy Markdown

@xkonjin xkonjin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review for PR 1838:
This looks like a solid fix for the Windows pipeline hang issues (#1482).

A couple of observations:

  1. Shortening the timeout to 500ms for 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.
  2. Skipping stdin for start is a great optimization to avoid the blocking wait altogether for daemon startup.
  3. The test coverage validates the presence of the logic in the script string (which I assume is how bun-runner.test.ts is structured, checking the source directly).

Code is clean and directly addresses the described issue.

@thedotmack
Copy link
Copy Markdown
Owner

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.

@thedotmack thedotmack closed this Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

claude-mem plugin breaks claude --print (pipe mode) on Windows

3 participants