Windows: SessionStart hooks that invoke bash fail (exit 127, bash: command not found)

Resolved 💬 0 comments Opened May 30, 2026 by KLPTechSys Closed Jun 1, 2026

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 (SessionStart hook check_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

  1. On Windows with Git Bash, enable the official semgrep plugin (v0.5.3).
  2. Start/resume/clear a session. Its hooks.json includes:

``json
{ "type": "command", "command": "/usr/bin/bash \"${CLAUDE_PLUGIN_ROOT}/scripts/check_version.sh\"" }
``

  1. Observe a SessionStart:startup hook error every 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 SessionStart error 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.

View original on GitHub ↗