Windows: SessionStart hooks that invoke bash fail (exit 127, bash: command not found)
Environment
- Claude Code v2.1.157 (CLI)
- Windows 11 Pro (10.0.26200)
- Git for Windows (user install):
C:\Users\<user>\AppData\Local\Programs\Git\usr\bin\bash.exe(GNU bash 5.2.37 msys) - Repro plugin: official semgrep plugin v0.5.3 (
SessionStarthookcheck_version.sh)
Summary
On Windows, any hook command whose first token resolves to bash fails with exit 127 and /usr/bin/bash: line 1: bash: command not found. The hook's target script never executes. Hook commands that are a bare, PATH-resolvable Windows executable work fine. This makes the official semgrep plugin throw a SessionStart error on every startup/resume/clear/compact.
Steps to reproduce
- On Windows with Git Bash, enable the official semgrep plugin (v0.5.3).
- Start/resume/clear a session. Its
hooks.jsonincludes:
``json``
{ "type": "command", "command": "/usr/bin/bash \"${CLAUDE_PLUGIN_ROOT}/scripts/check_version.sh\"" }
- Observe a
SessionStart:startup hook errorevery time.
Expected: the hook runs check_version.sh and exits 0.
Actual: exit 127, stderr /usr/bin/bash: line 1: bash: command not found. The script body never runs.
Decisive evidence
I tested three ways of spelling the bash invocation via temporary SessionStart hooks; all fail identically, while a bare executable in the same hook set succeeds:
| Hook command | Result |
|---|---|
| /usr/bin/bash "<script>" (semgrep's form) | exit 127, bash: command not found |
| bash "<script>" (bare) | exit 127, identical |
| "C:\Users\<user>\AppData\Local\Programs\Git\usr\bin\bash.exe" "<script>" (full Windows path) | exit 127, identical |
| semgrep mcp -k inject-secure-defaults (bare exe) | exit 0 |
Transcript hook records (attachment.type: hook_non_blocking_error):
exitCode: 127
command: /usr/bin/bash "${CLAUDE_PLUGIN_ROOT}/scripts/check_version.sh"
stderr: Failed with non-blocking status code: /usr/bin/bash: line 1: bash: command not found
A probe script that simply writes a file produced no file — confirming failure is upstream of the target script, at the bash invocation itself.
Analysis
Claude Code appears to wrap every hook command in Git Bash (/usr/bin/bash -c "<command>"). When <command> itself invokes bash (a nested/second bash), that nested invocation cannot be resolved — note the error reports the failed program as bare bash regardless of whether the command specified /usr/bin/bash or a full Windows path, suggesting the inner program token is normalized/mangled to bash and then not found on the wrapper's PATH. A bare exe like semgrep resolves via the Windows PATH and works, so it is specifically bash-invoking hook commands that break. (This is a regression in behavior: an earlier workaround of using absolute /usr/bin/bash previously worked on this box and no longer does.)
Impact
- The official semgrep plugin emits a
SessionStarterror on every session event on Windows. - Any plugin/user hook that shells out to a script via bash is broken on Windows. Non-blocking, but persistent and confusing.
Workaround
Remove the bash-invoking hook from the plugin's hooks.json (lives in the version-numbered plugin cache, so it's overwritten on plugin update). There is no per-hook disable in settings, so this recurs after updates.
Suggested fix
When executing hook commands on Windows, ensure the wrapper's environment can resolve a nested bash invocation (preserve bash/Git usr/bin on the hook PATH, or don't normalize the inner interpreter token), so command: "/usr/bin/bash <script>" works as plugin authors expect.