[BUG] Windows: model is told Shell=PowerShell while hooks execute under bash — cross-shell foot-gun for hook authors
Summary
On Windows, Claude Code's environment prelude shown to the model declares the primary shell as PowerShell:
Shell: PowerShell (use PowerShell syntax — e.g., \$null not /dev/null, \$env:VAR not \$VAR, backtick for line continuation). Bash is also available via the Bash tool for POSIX scripts.
…but hook commands in ~/.claude/settings.json are executed under /usr/bin/bash when Git Bash is present on PATH. The discrepancy isn't documented anywhere visible to hook authors. The model assumes PowerShell semantics; the hook runner uses bash. Anyone writing or installing hook commands (manually, via skills, via plugin installers) hits this.
Concrete failure observed
Third-party tool get-shit-done-cc (gsd) installed hook commands as PowerShell call-operator invocations:
\"command\": \"& \\\"C:/Users/kberger/node/node.exe\\\" \\\"C:/Users/kberger/.claude/hooks/gsd-workflow-guard.js\\\"\"
Every tool call fired:
/usr/bin/bash: -c: line 1: syntax error near unexpected token \`&'
PreToolUse hooks blocked Edit/Write entirely; PostToolUse and statusLine spammed the conversation with hundreds of bytes of error output per tool call. The gsd side is already filed and fixed in main: gsd-build/get-shit-done#3413. But the root cause — Claude Code silently disagreeing with its own environment prelude about which shell runs hook commands — is upstream.
Why this is a Claude Code bug, not just a gsd bug
- The model is actively misled. The system prompt tells me to use PowerShell syntax. A reasonable hook-installer skill following that guidance writes PowerShell-syntax commands. Those then fail because the hook runner uses bash. Any skill author on Windows can step on this.
- No documented contract. The hook docs don't say \"hook commands always execute under bash regardless of the user's interactive shell.\" If that's the intended behavior, it should be stated next to every hook example.
- Silent shell selection. Hook-runner shell appears to be selected by \"is Git Bash on PATH?\" with no override and no signal to the user. The same
~/.claude/settings.jsonworks or breaks based on whether the user happens to have Git installed.
Proposed fixes (pick any subset)
- Document the contract. Add to the hooks docs: \"Hook commands execute under
bashon Windows when Git Bash is available, falling back to cmd.exe otherwise. Write commands in POSIX shell syntax regardless of the user's interactive shell.\" - Align the environment prelude. When the prelude says \"Shell: PowerShell,\" also say \"Hook commands: bash\" so the model writes the right syntax when generating hook configs.
- Configurable hook shell. Honor a
hooks.shellsetting (bash|powershell|cmd) so users on PowerShell-only Windows boxes aren't forced to install Git just to satisfy the hook runner. - Better error surface. When a hook command fails to parse, surface the interpreter used so users can diagnose this in seconds instead of hours.
Environment
- OS: Windows 11 Pro 10.0.26200
- Claude Code: latest (running Opus 4.7)
- Git Bash present: yes (
/usr/bin/bash) - Interactive shell reported by harness: PowerShell
- Hook runner actually used: bash
Related
- gsd-build/get-shit-done#3413 (downstream tool affected; fix in main, not yet released)
- #7490 (feature request — configurable Bash-tool shell; adjacent but not the hook runner)
- #26200 (Windows + plugin hooks + bash path escaping; closed as duplicate — different symptom, same underlying \"hooks run under bash on Windows\" assumption)
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗