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

Resolved 💬 3 comments Opened Mar 19, 2026 by JordiSanchezS Closed Mar 19, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] 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):
```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_

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗