[Docs] Windows PowerShell hooks: exit code from .ps1 scripts not propagated to Claude Code
Documentation Type
Missing documentation (feature not documented)
Documentation Location
https://code.claude.com/docs/en/hooks
Section/Topic
Exit code output / Hook handler fields
Current Documentation
The docs provide only Bash/shell examples for hooks. The "Exit code output" section shows a bash script example but no PowerShell equivalent. The docs say:
"Exit 2 means a blocking error. stderr text is fed back to Claude as an error message."
But there is no mention of how to achieve this on Windows with PowerShell scripts.
What's Wrong or Missing?
On Windows, when calling a .ps1 script from settings.json using powershell -Command "& script.ps1", the script's exit 2 does NOT propagate to the PowerShell process exit code. The process always exits 0, so Claude Code never sees the blocking signal.
Both & $f (call operator) and . $f (dot-source) fail to propagate exit codes.
The only working pattern is: & script.ps1; exit $LASTEXITCODE
This is undocumented and took hours of debugging to discover. Windows users attempting to write PreToolUse hooks that block commands will silently fail with no indication of what's wrong.
Reproduction:
- Create test-hook.ps1:
[Console]::Error.WriteLine("BLOCKED"); exit 2 - Register in settings.json with:
powershell -Command "& test-hook.ps1" - Hook fires but exit code 2 is lost — commands are NOT blocked
- Change to:
powershell -Command "& test-hook.ps1; exit $LASTEXITCODE" - Now commands ARE blocked correctly
Suggested Improvement
Add a "Windows / PowerShell" section or tab to the hooks reference with:
- The required
; exit $LASTEXITCODEpattern:
{
"command": "powershell -ExecutionPolicy Bypass -Command \"& (Join-Path $env:USERPROFILE 'hooks/block.ps1'); exit $LASTEXITCODE\""
}
### Impact
High - Prevents users from using a feature
### Additional Context
Add a "Windows / PowerShell" section to the hooks reference covering the exit code propagation gotcha.
When calling a .ps1 script via "powershell -Command", the script's exit code is silently lost. The required pattern is to append "; exit $LASTEXITCODE" after the script call:
"command": "powershell -ExecutionPolicy Bypass -Command \"& (Join-Path $env:USERPROFILE 'hooks/block.ps1'); exit $LASTEXITCODE\""
Include a minimal PowerShell hook example equivalent to the existing bash block-rm.sh:
$data = [Console]::In.ReadToEnd() | ConvertFrom-Json
$command = $data.tool_input.command
if ($command -match 'rm\s+-rf') {
[Console]::Error.WriteLine("Destructive command blocked")
exit 2
}
exit 0
Also note that $env:USERPROFILE works in hook commands because hooks run via the system shell (cmd.exe on Windows), not Git Bash, so dollar signs are not consumed.This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗