superpowers/hooks/run-hook.cmd
Drew Ritter 7e2e91942b
Merge pull request #2594 from obra/fix/2503-run-hook-no-bash
fix(hooks): fall back to zsh, or exit quietly, when bash is missing on Unix (#2503)
2026-10-22 15:05:26 -07:00

79 lines
3.5 KiB
Batchfile
Executable file

: << 'CMDBLOCK'
@echo off
REM Cross-platform polyglot wrapper for hook scripts.
REM On Windows: cmd.exe runs the batch portion, which finds and calls bash.
REM On Unix: the shell interprets this as a script (: is a no-op in bash).
REM
REM Hook scripts use extensionless filenames (e.g. "session-start" not
REM "session-start.sh") so Claude Code's Windows auto-detection -- which
REM prepends "bash" to any command containing .sh -- doesn't interfere.
REM
REM Usage: run-hook.cmd <script-name> [args...]
if "%~1"=="" (
echo run-hook.cmd: missing script name >&2
exit /b 1
)
setlocal
set "HOOK_DIR=%~dp0"
set "BASH_EXE="
REM Git for Windows in its standard system-wide locations
if exist "C:\Program Files\Git\bin\bash.exe" set "BASH_EXE=C:\Program Files\Git\bin\bash.exe"
if not defined BASH_EXE if exist "C:\Program Files (x86)\Git\bin\bash.exe" set "BASH_EXE=C:\Program Files (x86)\Git\bin\bash.exe"
REM Per-user Git for Windows install (no admin rights needed). The defined
REM check matters: with LOCALAPPDATA unset the path would collapse to
REM \Programs\Git\... on the current drive, where any user can create it.
if not defined BASH_EXE if defined LOCALAPPDATA if exist "%LOCALAPPDATA%\Programs\Git\bin\bash.exe" set "BASH_EXE=%LOCALAPPDATA%\Programs\Git\bin\bash.exe"
REM bash on PATH (MSYS2, Cygwin, a non-default Git install). where.exe is
REM called by full path and with $PATH: so neither it nor bash can come from
REM the current directory. The filters use for-variable modifiers, which never
REM re-expand the path, and skip extensionless matches and the WSL launchers
REM (System32, Sysnative, the Store alias in WindowsApps), which fail when no
REM Linux distro is installed.
if not defined BASH_EXE if defined SystemRoot for /f "delims=" %%B in ('"%SystemRoot%\System32\where.exe" $PATH:bash 2^>nul') do if not defined BASH_EXE if not "%%~xB"=="" if /i not "%%~dpB"=="%SystemRoot%\System32\" if /i not "%%~dpB"=="%SystemRoot%\Sysnative\" if /i not "%%~dpB"=="%LOCALAPPDATA%\Microsoft\WindowsApps\" set "BASH_EXE=%%B"
REM No bash found - exit silently rather than error
REM (plugin still works, just without SessionStart context injection)
if not defined BASH_EXE exit /b 0
REM Run bash outside any parenthesized block: cmd expands %ERRORLEVEL%
REM inside a block when it parses the block, which would lose the hook's
REM exit code.
"%BASH_EXE%" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
CMDBLOCK
# Unix: run the named script directly
# Splits $0 on its last / or \ (cmd passes Windows paths) instead of calling
# dirname, and uses $BASH (bash's own absolute path, always set once bash is
# running) instead of a PATH lookup for bash, so this keeps working when
# Claude Code spawns the SessionStart hook with a broken/empty PATH
# (anthropics/claude-code#43127).
# No $(cd ... && pwd) subshell: a fork costs tens of milliseconds under Git
# Bash on Windows, and a relative directory is enough for the exec below.
case "$0" in
*/*|*\\*) SCRIPT_DIR="${0%[/\\]*}" ;;
*) SCRIPT_DIR="." ;;
esac
SCRIPT_NAME="$1"
shift
# Without bash installed, the harness runs this file with zsh or sh and $BASH
# is unset (#2503). session-start runs unchanged under zsh, so fall back to
# it, and otherwise exit quietly like the Windows half: the plugin still
# works, just without SessionStart context injection.
if [ -n "${BASH:-}" ]; then
shell="$BASH"
elif command -v bash >/dev/null 2>&1; then
shell="bash"
elif command -v zsh >/dev/null 2>&1; then
shell="zsh"
else
exit 0
fi
exec "$shell" "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"