Claude Code hooks not working on Windows: causes and fixes

The same hook that runs fine on a Mac does nothing on Windows, or worse, blocks every command. Almost always it is the shell. Here is how Claude Code runs hooks on Windows and how to make yours reliable.

By Aslan, Pet Island team · Updated Oct 26, 2026 · 5 min read

SHORT ANSWER

On Windows, Claude Code runs a hook’s command with Git Bash, or with PowerShell when Git Bash isn’t installed. Say which one you mean with "shell": "powershell" or "shell": "bash", keep stdout to one JSON object (no profile echoes), use exit code 2 to block, and update Claude Code (several Windows hook bugs were fixed in versions up to 2.1.310). Debug with /hooks and claude --debug.

Which shell runs your hook

Claude Code passes the command string to a shell: sh -c on macOS and Linux, Git Bash on Windows, or PowerShell when Git Bash isn’t installed. That one sentence from the hooks reference explains most “works on my Mac” reports. A command written for PowerShell fails in Git Bash, a bash script fails in PowerShell, and which one you get depends on whether Git for Windows happens to be installed.

Take the guessing out with the shell field. It accepts "bash" or "powershell":

%USERPROFILE%\.claude\settings.jsonCopy
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [
{
"type": "command",
"shell": "powershell",
"command": "[Console]::Beep(880, 200)"
}
]
}
]
}
}

If you want Git Bash and Claude Code can’t find it, install Git for Windows or set CLAUDE_CODE_GIT_BASH_PATH to its location, then start a new session.

Symptoms and what causes them

What you seeLikely causeFix
The hook never seems to runOld Claude Code version, wrong event or matcher, invalid JSON in settingsUpdate; check /hooks; remove comments and trailing commas
ParserError or “unexpected token”PowerShell syntax reaching Git Bash, or the reverseSet shell explicitly
Hook output “does not start with {”Something printed before the JSON, often a shell profileSilence profile output; send logs to stderr
A path with mixed \ and / can’t be foundWindows path variable joined with a Unix-style suffixUse exec form: program in command, each argument in args
powershell: Permission denied from Git BashReported intermittently in 2.1.283 and 2.1.304–2.1.305 (#103910, open)Use "shell": "powershell" so Git Bash isn’t in between
A guard hook lets everything throughExit code 1 instead of 2, or a fail-open bug in older versionsExit 2 to block; update to 2.1.310 or later
The hook returns instantly with no outputA GUI-subsystem program that the shell doesn’t wait forUse a console program, or wait for the process

Only JSON on stdout

Claude Code reads your hook’s standard output as its answer. The rule in the docs is short: print nothing but the JSON object to stdout. On Windows this breaks in a sneaky way: Git Bash and some setups still source your shell profile, and an unconditional echo in that profile gets prepended to your JSON. The hook then “works” in a terminal and fails inside Claude Code.

Log to a file or to stderr instead, and make your profile’s output conditional on an interactive shell.

Exit codes: 2 blocks, 1 doesn’t

Exit 0 is success. Exit 2 is a blocking error: on events that can block, Claude Code stops the action and shows your stderr. Exit 1 without valid JSON is a non-blocking error, and the action goes ahead. If your hook enforces a policy, use exit 2.

Not every event can be blocked this way. A Notification hook’s exit code is ignored. A Stop hook that exits 2 keeps Claude working. A PermissionRequest hook ignores exit 2 entirely. It decides through a decision object in its JSON.

Claude Code 2.1.309 fixed PowerShell hooks not blocking when the hook’s program exited with code 2. If your PowerShell guard seems ignored, check your version first.

Windows fixes worth updating for

Anthropic’s changelog has a long trail of Windows hook fixes. The ones that explain old forum threads:

VersionFix
2.1.47PreToolUse and PostToolUse hooks silently failed on Windows; they now run with Git Bash instead of cmd.exe
2.1.161Hooks that call bash explicitly (/usr/bin/bash script.sh) failed with “command not found”
2.1.299PreToolUse and PermissionRequest hooks with no shell set failed open without Git for Windows; a failing guard now blocks
2.1.309PowerShell hooks didn’t block on exit code 2; "shell": "powershell" guards failed open without Git for Windows
2.1.310Guards whose command was only quoted strings (such as a quoted path) failed open without Git Bash

The older issue titled “No hook is working on Windows” (#10450) was reported on 2.0.27; a commenter traced it to stdin not reaching hook commands. If you are on a version that old, update before debugging anything else.

Calling a program directly

If your hook is a compiled program, you can skip the shell with the exec form (the args field; shell is ignored when args is set). On Windows, exec form requires command to resolve to a real executable such as a .exe; .cmd and .bat shims (including most things npm installs) can’t be spawned without a shell. Point at the .exe, or use the shell form.

One lesson from building our own hook helper: on Windows, a program built for the GUI subsystem is not waited for when a shell starts it. The shell returns at once, the agent reads empty output, and the hook looks dead. Hook programs must be console programs.

Plugin hooks and paths

Plugin hooks that build paths like ${CLAUDE_PLUGIN_ROOT}/hooks/run.sh broke on Windows between v2.1.2 and v2.1.12 (#18610): the variable expands to a backslash path, and adding a forward-slash suffix gives a mixed path bash can’t resolve. The current docs give the clean fix: use exec form whenever a hook references a path placeholder. Put the program in command and each argument in args, for example "command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/format.js"]. Each element is passed as one argument, with no shell to mangle it.

Debugging checklist

  1. /hooks opens a read-only list of every configured hook and where it comes from.
  2. claude --debug (or /debug mid-session) shows each hook run, its output and exit code. claude --debug-file <path> writes it to a file.
  3. Validate the JSON. Settings files are strict JSON; one trailing comma disables the whole file.
  4. Check the matcher. Matchers are case-sensitive; "*", "" or no matcher matches everything.
  5. Run the command yourself in the same shell Claude Code will use, with sample JSON piped in.
  6. Look at the timeout. Command hooks default to 600 seconds; a hook that waits for a person needs that headroom.

To switch every hook off while you investigate, set "disableAllHooks": true.

If you only wanted notifications and approvals

Many Windows hook setups exist for one reason: to know when Claude Code needs you and to answer it. Pet Island does that without any scripting. It connects to Claude Code in one click, handles the Windows details for you, and puts every permission request, question and plan on a small island at the top of the screen, with the command or the diff, where you allow or deny it. It also works when Claude Code runs inside WSL.

FAQ

Which shell runs Claude Code hooks on Windows?

Git Bash, or PowerShell when Git Bash isn’t installed. Set "shell": "powershell" or "shell": "bash" in the hook to choose explicitly.

Why does my Claude Code hook work in the terminal but not in Claude Code?

Usually extra output: a shell profile that echoes text gets prepended to the hook’s JSON. Print only the JSON object to stdout.

How do I make a hook block a tool call?

Exit with code 2 (or return a deny decision in JSON). Exit code 1 is a non-blocking error and the action goes ahead. PermissionRequest hooks ignore exit 2 and use a decision object instead.

Can a hook command be a .cmd or .bat file?

Not in exec form. On Windows it must resolve to a real executable such as a .exe. Use the shell form to run .cmd or .bat files.

How do I see why a hook failed?

Run claude --debug or type /debug, and use /hooks to see which hooks are loaded.

Sources

Every setting and behavior on this page was checked against these pages on October 26, 2026. Agents change fast; if something no longer matches, the official page wins.

KEEP READING

Answer every agent from the top of your screen.

Windows and Mac · 7 days free, then $8.99 once · $1 of every sale feeds street animals

Try Pet Island free