A minimal, working Node.js template for Claude Code hooks on Windows.
Bypasses the documented bash-hook bugs on Windows Claude Code (.sh scripts
not executing, stdin delivered as TTY instead of pipe, shell setting
ignored, etc.) by using Node.js 鈥?which Anthropic's official docs already
recommend for cross-platform hook authoring, but without providing a concrete
working example.
If you try to write a Claude Code hook on Windows with a bash .sh script,
you'll likely hit one or more of these open / unfixed issues:
| Issue | Symptom |
|---|---|
| #24097 | .sh hooks trigger a file-association dialog instead of executing |
| #32930 | settings.json shell setting is ignored; hooks hardcoded to /usr/bin/bash |
| #36156 | stdin is delivered as TTY instead of pipe 鈥?cat / jq hang or return empty |
| #46601 | Stop hook stdin bug on PowerShell 5.1 / pwsh 7 |
| #9758 | Closed as NOT_PLANNED 鈥?path handling never fixed at the Claude Code level |
Existing community workarounds are partial: AutoHotkey scripts that hide bash
popup windows, CLAUDE_CODE_GIT_BASH_PATH environment variable tricks,
.bashrc + cygpath rewiring. They fix one symptom each but no single
recipe that works for all hook types.
Anthropic's hooks guide notes that hooks invoked via node work uniformly
across Windows / Linux / macOS, whereas platform-specific shells do not.
This repo is a concrete working implementation of that recommendation,
tested on a real Windows 11 Claude Code install. It handles the edge cases
that don't appear in the one-line docs mention:
- stdin TTY fallback 鈥?if stdin is a TTY (Windows #36156 bug), skip reading
(which would hang) and fall back to
CLAUDE_SESSION_IDenv var - MSYS path normalization 鈥?Git Bash passes paths as
/c/Users/...but Node's nativefsneedsC:/Users/...; a small helper normalizes - Detached worker pattern 鈥?heavy work (API calls, git push) runs in a detached child process so the hook returns in milliseconds and Claude's response is never blocked
- env-based worker communication 鈥?stdin can only be read once; the worker gets its params from env vars set at spawn time
- Copy
hook-template.mjsto a location on your machine (e.g.~/.claude/hooks/my-hook.mjs) - Replace the
runWorker()body with your business logic - Register in
~/.claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node C:/Users/YOU/.claude/hooks/my-hook.mjs"
}
]
}
]
}
}Works for Stop, SessionEnd, PreToolUse, PostToolUse, and other hook
events. The same pattern applies.
| Approach | Works on Windows? | Caveats |
|---|---|---|
.sh script directly in hook |
鉁? | Hits #24097 / #36156 鈥?file association dialog or empty stdin |
.sh + CLAUDE_CODE_GIT_BASH_PATH env |
鈿狅笍 partially | Only works if Git Bash is in an exact path; doesn't fix stdin TTY bug fundamentally |
| PowerShell hook | 鈿狅笍 partially | Works for some hook events but has its own stdin quirks (#46601) |
| Node.js (this template) | 鉁? | Works uniformly; Node is already required for Claude Code |
Extracted from a real multi-machine workflow running Claude Code on both Linux and Windows. The Linux side worked out of the box with bash; the Windows side required this Node.js rewrite to handle the hook stdin / path quirks cleanly.
MIT 鈥?use freely, modify, redistribute.
Found another Windows hook quirk not handled here? Open an issue or PR.