Preflight Checklist
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_
Preflight Checklist
What's Wrong?
Environment
Description
On Windows, hook commands always receive
process.stdin.isTTY === true, meaning stdin isconnected to a pseudo-terminal (PTY) instead of a pipe. As a result, hooks never receive
any tool input data —
process.stdinemits nodataevents and theendevent neverfires. 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_