[BUG] Hook command on Windows: backslashes in absolute paths are shell-mangled before execution
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
On Windows, when a PreToolUse (or any hook) command string contains a Windows absolute path with backslashes — e.g. pointing at a .ps1 script — the harness pipes the command through /usr/bin/bash, which interprets \U, \A, \g, etc. as unknown backslash escapes and silently strips the backslashes. The downstream process therefore receives a malformed path and fails to launch.
The hook still registers and fires (it shows up in --include-hook-events stream), but the spawned process can't find its target file and dies with exit 127. From the user's perspective the hook "doesn't work" with no obvious diagnostic.
What Should Happen?
Backslashes in hook command strings should be preserved verbatim on Windows, so absolute paths to scripts (.ps1, .cmd, .exe) work without escape acrobatics. Either:
- Use a Windows-aware shell to invoke the command (cmd.exe / pwsh) on Windows, OR
- Pre-escape backslashes before handing to bash, OR
- Document the bash-on-Windows behavior prominently in the hooks docs and recommend forward slashes.
Today's behavior makes the natural Windows JSON spelling ("C:\\Users\\me\\hook.ps1") silently break.
Error Messages/Logs
Running claude.exe -p "..." --include-hook-events --output-format=stream-json shows the hook firing and the spawned process's failure verbatim. With the hook command set to:
powershell.exe -ExecutionPolicy Bypass -File C:\\Users\\<user>\\probe.ps1 fired
The stream-json output reports:
{
"type": "system",
"subtype": "hook_response",
"hook_name": "PreToolUse:Bash",
"hook_event": "PreToolUse",
"stderr": "The argument 'C:Users<user>probe.ps1' to the -File parameter does not exist. Provide the path to an existing '.ps1' file as an argument to the -File parameter.\r\n",
"exit_code": 127,
"outcome": "error"
}
Note the path the harness handed to powershell.exe: C:Users<user>probe.ps1 — every backslash has been stripped. Original config had C:\\Users\\<user>\\probe.ps1 (the standard JSON-escaped form).
Steps to Reproduce
- On Windows, save the following stub script as
C:\Users\<user>\probe.ps1:
``powershell``
"$args" | Out-File -FilePath "$env:TEMP\probe-fired.txt" -Encoding UTF8 -NoNewline
- Put this in
~/.claude/settings.json:
``json``
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "powershell.exe -ExecutionPolicy Bypass -File C:\\Users\\<user>\\probe.ps1 fired"
}
]
}
]
}
}
- Reload window (or start a fresh session) and have Claude run any Bash tool call.
- Observed:
%TEMP%\probe-fired.txtis never created. The hook reports an error in stream-json output: PowerShell can't find a file at the de-escaped path (C:Users<user>probe.ps1). - Workaround: change the
commandto use forward slashes —C:/Users/<user>/probe.ps1— and the script runs as expected. Forward slashes survive bash escape-stripping unchanged.
Claude Code Version
2.1.123 (VSCode extension, bundled CLI)
Platform
Anthropic API (subscription)
Operating System
Windows Server 2025 Datacenter (10.0.26100). Reproduces on the VSCode extension's Native UI mode and via direct invocation of the bundled claude.exe -p ....
Terminal/Shell
VSCode integrated; harness shell is the bundled bash from Git for Windows / WSL-style /usr/bin/bash.
Additional Information
- This is independent of issue #8985 (which is specifically about
Notification/PermissionRequestsubtype handlers missing). Per the source-code analysis in that thread,PreToolUseis supposed to work in the extension — and it does, the hook absolutely fires. The bug is purely in how thecommandstring is dispatched on Windows. - The forward-slash workaround works because PowerShell's
-Fileparameter accepts/-separated paths on Windows. - Suggested doc fix even before a code fix: add a "Windows: use forward slashes in hook command paths" note to the hooks reference, since it's the kind of footgun that costs hours to diagnose without
--include-hook-events --output-format=stream-json(which is itself non-obvious).
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗