Skip to content

[Bug] readStdin() hangs indefinitely on Windows - no timeout on stdin end event #25

@aka-kool

Description

@aka-kool

Bug Description

The SessionStart and Stop hooks cause Claude Code to hang for the full 30-second hook timeout on Windows because readStdin() in src/lib/stdin.js has no timeout. It waits indefinitely for process.stdin to emit an end event, which never fires on Windows when Claude Code spawns the hook subprocess.

Root Cause

The original readStdin() implementation:

async function readStdin() {
  return new Promise((resolve, reject) => {
    let data = '';
    process.stdin.setEncoding('utf8');
    process.stdin.on('data', (chunk) => { data += chunk; });
    process.stdin.on('end', () => {
      try {
        resolve(data.trim() ? JSON.parse(data) : {});
      } catch (err) {
        reject(new Error(`Failed to parse stdin JSON: ${err.message}`));
      }
    });
    process.stdin.on('error', reject);
    if (process.stdin.isTTY) resolve({});
  });
}

On Windows (Git Bash / MSYS2), the stdin end event is never emitted for hook subprocesses. The isTTY check doesn't help because stdin is piped (not a TTY), so the promise never resolves. The script hangs at the very first line of execution — before authentication or API calls even begin — and consumes the entire 30-second hook timeout.

Suggested Fix

Add a configurable timeout to readStdin() so it resolves with whatever data has been received (or an empty object) after a reasonable wait:

async function readStdin(timeoutMs = 3000) {
  return new Promise((resolve, reject) => {
    let data = '';
    let resolved = false;

    function finish(value) {
      if (resolved) return;
      resolved = true;
      clearTimeout(timer);
      resolve(value);
    }

    const timer = setTimeout(() => {
      try {
        finish(data.trim() ? JSON.parse(data) : {});
      } catch (err) {
        finish({});
      }
    }, timeoutMs);

    process.stdin.setEncoding('utf8');
    process.stdin.on('data', (chunk) => { data += chunk; });
    process.stdin.on('end', () => {
      try {
        finish(data.trim() ? JSON.parse(data) : {});
      } catch (err) {
        if (!resolved) {
          resolved = true;
          clearTimeout(timer);
          reject(new Error(`Failed to parse stdin JSON: ${err.message}`));
        }
      }
    });
    process.stdin.on('error', (err) => {
      if (!resolved) {
        resolved = true;
        clearTimeout(timer);
        reject(err);
      }
    });
    if (process.stdin.isTTY) finish({});
  });
}

Secondary Issue: ${CLAUDE_PLUGIN_ROOT} not resolved on Windows

The hook commands in hooks.json use ${CLAUDE_PLUGIN_ROOT}:

"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.cjs\""

On Windows with Git Bash / MSYS2, ${CLAUDE_PLUGIN_ROOT} is not expanded by the shell when Claude Code executes the hook command. This appears to be a Claude Code issue — the variable is either not set in the hook's execution environment, or the command string is not processed through shell expansion before execution.

This is likely a Claude Code platform bug (not specific to this plugin) that affects any plugin using ${CLAUDE_PLUGIN_ROOT} in hook commands on Windows. It may need to be reported to Anthropic separately.

As a workaround, we replaced the hook commands with hardcoded absolute paths:

"command": "volta run node ~/.claude/plugins/marketplaces/supermemory-plugins/plugin/scripts/context-hook.cjs"

Environment

  • OS: Windows 11 (Git Bash / MSYS2 shell)
  • Node runner: Volta
  • Plugin version: 0.0.1 (commit e323919)
  • Claude Code: Latest

Steps to Reproduce

  1. Install the plugin on Windows
  2. Restart Claude Code
  3. Observe ~30 second hang on every startup
  4. Debug logs show the hook timing out before any authentication or API activity

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions