Skip to content

[Windows] Hooks receive stdin as TTY instead of pipe — tool input never received #36156

Description

@JordiSanchezS

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • I am using the latest version of Claude Code

What's Wrong?

Environment

  • OS: Windows 11 Enterprise
  • Claude Code version: (latest)
  • Shell: Git for Windows (git-bash.exe / bash.exe)

Description

On Windows, hook commands always receive process.stdin.isTTY === true, meaning stdin is
connected to a pseudo-terminal (PTY) instead of a pipe. As a result, hooks never receive
any tool input data — process.stdin emits no data events and the end event never
fires. The 500ms fallback timeout resolves with 0 bytes.

This makes it impossible to implement PreToolUse/PostToolUse hooks that inspect tool input
(e.g. blocking reads of sensitive files, checking file paths before writes, etc.).

What Should Happen?

Hook processes should receive tool input as JSON on stdin (a proper pipe), matching the
behaviour on Linux/macOS and the documented hook contract:
{
"tool_name": "Read",
"tool_input": { "file_path": "/path/to/file" },
...
}

Error Messages/Logs

Steps to Reproduce

Reproduction

Hook command (settings.json):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read",
        "hooks": [{ "type": "command", "command": "node ./hooks/read_hook.js" }]
      }
    ]
  }
}

Hook script (read_hook.js, ES module):
import { writeFileSync } from 'fs';

async function main() {
  writeFileSync('debug.txt', `isTTY=${process.stdin.isTTY}\n`);

  const raw = await new Promise((resolve) => {
    let data = '';
    process.stdin.on('data', (chunk) => (data += chunk));
    process.stdin.on('end', () => resolve(data));
    setTimeout(() => resolve(data), 500); // fallback
  });

  writeFileSync('debug.txt', `bytes received: ${raw.length}\ndata: ${raw}\n`);
  process.exit(0);
}

main();

Result in debug.txt after hook fires:
isTTY=true
bytes received: 0
data:

Investigation

- process.stdin.isTTY === true confirms stdin is a PTY, not a pipe
- CLAUDE_CODE_GIT_BASH_PATH env var is visible inside the hook — Claude Code does
inject it correctly
- Changing CLAUDE_CODE_GIT_BASH_PATH in ~/.claude/settings.json from
git-bash.exe to bin\bash.exe (non-MinTTY) is respected, but isTTY remains
true — the PTY behaviour is not resolved by this change
- readFileSync(0, 'utf8') (synchronous stdin read) causes a visible terminal/console
window to open, waiting for user keyboard input — confirming stdin is attached to
a live console device, not a pipe

### Claude Model

Sonnet (default)

### Is this a regression?

I don't know

### Last Working Version

_No response_

### Claude Code Version

2.1.77 (Claude Code)

### Platform

Anthropic API

### Operating System

Windows

### Terminal/Shell

PowerShell

### Additional Information

_No response_

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:hooksbugSomething isn't workinghas reproHas detailed reproduction stepsplatform:windowsIssue specifically occurs on Windows

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions