fix: restore polyglot wrapper for Windows hook compatibility (4.3.1)#523
fix: restore polyglot wrapper for Windows hook compatibility (4.3.1)#523
Conversation
Claude Code spawns hook commands with shell:true + windowsHide:true, but on Windows the execution chain cmd.exe -> bash.exe causes Git Bash (MSYS2) to allocate its own console window, bypassing the hide flag. This creates visible terminal windows that steal focus on every SessionStart event (startup, resume, clear, compact). The fix: - Rename session-start.sh to session-start (no extension) so Claude Code's .sh auto-detection regex doesn't fire and prepend "bash" - Restore run-hook.cmd polyglot wrapper to control bash invocation on Windows (tries known Git Bash paths, then PATH, then exits silently if no bash found) - On Unix, the polyglot's shell portion runs the script directly This avoids Claude Code's broken .sh auto-prepend, gives us control over how bash is invoked on Windows, and gracefully handles missing bash instead of erroring. Addresses: #440, #414, #354, #417, #293 Upstream: anthropics/claude-code#14828
📝 WalkthroughWalkthroughSessionStart hook invocation was changed to run through a Windows CMD wrapper and the wrapper was rewritten to locate and invoke Bash from multiple locations (with PATH fallback and silent no-bash exit). Added LF line-ending normalization for the session-start hook. Changes
Sequence Diagram(s)sequenceDiagram
participant Host as Host process (hooks runner)
participant CMD as run-hook.cmd
participant Bash as Git Bash (bash.exe / bash)
participant Script as session-start.sh
Host->>CMD: Invoke run-hook.cmd session-start
CMD->>Bash: Search standard paths\n(C:\Program Files\Git\bin, C:\Program Files (x86)\Git\bin, PATH)
alt Bash found
CMD->>Bash: Execute bash "${SCRIPT_DIR}/session-start.sh" with args
Bash->>Script: run script
Script-->>Bash: exit code
Bash-->>CMD: returns exit code
CMD-->>Host: return same exit code
else Bash not found
CMD-->>Host: exit 0 (silent fallback)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@hooks/hooks.json`:
- Around line 8-10: The hook command string for the session-start hook uses
${CLAUDE_PLUGIN_ROOT} without quotes which breaks on paths containing spaces;
update the "command" value in hooks/hooks.json (the entry with "command":
"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd session-start" for the session-start
hook) to wrap the expanded path in single quotes so the shell treats the full
path as one token (e.g., change to use '${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd
session-start').
In `@hooks/run-hook.cmd`:
- Around line 42-46: The line setting SCRIPT_DIR uses the bash-only parameter
expansion BASH_SOURCE and breaks under /bin/sh; update the SCRIPT_DIR assignment
in run-hook.cmd to POSIX-safe path resolution by using dirname "$0" (e.g.,
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)") and remove the BASH_SOURCE usage,
keeping the rest (SCRIPT_NAME, shift, exec bash ...) unchanged so the hook works
when invoked by /bin/sh on Debian/Ubuntu.
- Quote ${CLAUDE_PLUGIN_ROOT} in hooks.json to handle paths with spaces
(e.g. "C:\Users\Robert Zimmermann\...")
- Replace bash-only ${BASH_SOURCE[0]:-$0} with POSIX-safe $0 in
run-hook.cmd so the Unix path doesn't break on dash (/bin/sh)
Addresses: #518 (spaces in path), Ubuntu/Debian compatibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@hooks/run-hook.cmd`:
- Around line 42-46: The Unix branch currently tries to exec a path even when
SCRIPT_NAME is empty; add the same missing-argument guard used in the CMD block
by checking if SCRIPT_NAME is unset/empty (e.g., test -z "$SCRIPT_NAME"),
printing a clear error/usage message and exiting non‑zero, before using
SCRIPT_DIR, SCRIPT_NAME and the exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
call so the behavior matches the CMD block.
- Around line 18-34: The script currently invokes bash with individual
positional parameters (%2 %3 ... %9) which loses arguments beyond 9 and breaks
quoted arguments; update the invocation logic (the calls that run bash
"%HOOK_DIR%%~1" %2 %3 ... %9) to first shift once (so %1 becomes the script
path) and then pass all remaining original arguments using %* (preserving
quoting and unlimited args) when calling bash, and apply the same shift + %*
change for both "C:\Program Files\Git\bin\bash.exe", "C:\Program Files
(x86)\Git\bin\bash.exe", and the PATH bash branch that reference %HOOK_DIR%%~1
so HOOK_DIR, the bash invocation sites, shift, and %* are the elements to
change.
| set "HOOK_DIR=%~dp0" | ||
|
|
||
| REM Try Git for Windows bash in standard locations | ||
| if exist "C:\Program Files\Git\bin\bash.exe" ( | ||
| "C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9 | ||
| exit /b %ERRORLEVEL% | ||
| ) | ||
| if exist "C:\Program Files (x86)\Git\bin\bash.exe" ( | ||
| "C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9 | ||
| exit /b %ERRORLEVEL% | ||
| ) | ||
|
|
||
| REM Try bash on PATH (e.g. user-installed Git Bash, MSYS2, Cygwin) | ||
| where bash >nul 2>nul | ||
| if %ERRORLEVEL% equ 0 ( | ||
| bash "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9 | ||
| exit /b %ERRORLEVEL% |
There was a problem hiding this comment.
Preserve all arguments and keep quoted args intact.
Using %2 %3 ... %9 truncates arguments beyond 9 and breaks any argument containing spaces. Shift once and forward %* instead so all arguments (with original quoting) are preserved.
💡 Suggested fix (shift + %*)
set "HOOK_DIR=%~dp0"
+set "SCRIPT_NAME=%~1"
+shift
REM Try Git for Windows bash in standard locations
if exist "C:\Program Files\Git\bin\bash.exe" (
- "C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
+ "C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%SCRIPT_NAME%" %*
exit /b %ERRORLEVEL%
)
if exist "C:\Program Files (x86)\Git\bin\bash.exe" (
- "C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
+ "C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%SCRIPT_NAME%" %*
exit /b %ERRORLEVEL%
)
...
if %ERRORLEVEL% equ 0 (
- bash "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
+ bash "%HOOK_DIR%%SCRIPT_NAME%" %*
exit /b %ERRORLEVEL%
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| set "HOOK_DIR=%~dp0" | |
| REM Try Git for Windows bash in standard locations | |
| if exist "C:\Program Files\Git\bin\bash.exe" ( | |
| "C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9 | |
| exit /b %ERRORLEVEL% | |
| ) | |
| if exist "C:\Program Files (x86)\Git\bin\bash.exe" ( | |
| "C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9 | |
| exit /b %ERRORLEVEL% | |
| ) | |
| REM Try bash on PATH (e.g. user-installed Git Bash, MSYS2, Cygwin) | |
| where bash >nul 2>nul | |
| if %ERRORLEVEL% equ 0 ( | |
| bash "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9 | |
| exit /b %ERRORLEVEL% | |
| set "HOOK_DIR=%~dp0" | |
| set "SCRIPT_NAME=%~1" | |
| shift | |
| REM Try Git for Windows bash in standard locations | |
| if exist "C:\Program Files\Git\bin\bash.exe" ( | |
| "C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%SCRIPT_NAME%" %* | |
| exit /b %ERRORLEVEL% | |
| ) | |
| if exist "C:\Program Files (x86)\Git\bin\bash.exe" ( | |
| "C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%SCRIPT_NAME%" %* | |
| exit /b %ERRORLEVEL% | |
| ) | |
| REM Try bash on PATH (e.g. user-installed Git Bash, MSYS2, Cygwin) | |
| where bash >nul 2>nul | |
| if %ERRORLEVEL% equ 0 ( | |
| bash "%HOOK_DIR%%SCRIPT_NAME%" %* | |
| exit /b %ERRORLEVEL% | |
| ) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@hooks/run-hook.cmd` around lines 18 - 34, The script currently invokes bash
with individual positional parameters (%2 %3 ... %9) which loses arguments
beyond 9 and breaks quoted arguments; update the invocation logic (the calls
that run bash "%HOOK_DIR%%~1" %2 %3 ... %9) to first shift once (so %1 becomes
the script path) and then pass all remaining original arguments using %*
(preserving quoting and unlimited args) when calling bash, and apply the same
shift + %* change for both "C:\Program Files\Git\bin\bash.exe", "C:\Program
Files (x86)\Git\bin\bash.exe", and the PATH bash branch that reference
%HOOK_DIR%%~1 so HOOK_DIR, the bash invocation sites, shift, and %* are the
elements to change.
| # Unix: run the named script directly | ||
| SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| SCRIPT_NAME="$1" | ||
| shift | ||
| "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@" | ||
| exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@" |
There was a problem hiding this comment.
Mirror the missing-argument guard on Unix.
The CMD block exits with an error when no script name is provided, but the Unix block will attempt to execute a directory and fail with a generic error. Add a simple guard for parity.
💡 Suggested fix
# Unix: run the named script directly
+if [ -z "$1" ]; then
+ echo "run-hook.cmd: missing script name" >&2
+ exit 1
+fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_NAME="$1"
shift
exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Unix: run the named script directly | |
| SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | |
| SCRIPT_NAME="$1" | |
| shift | |
| "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@" | |
| exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@" | |
| # Unix: run the named script directly | |
| if [ -z "$1" ]; then | |
| echo "run-hook.cmd: missing script name" >&2 | |
| exit 1 | |
| fi | |
| SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | |
| SCRIPT_NAME="$1" | |
| shift | |
| exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@hooks/run-hook.cmd` around lines 42 - 46, The Unix branch currently tries to
exec a path even when SCRIPT_NAME is empty; add the same missing-argument guard
used in the CMD block by checking if SCRIPT_NAME is unset/empty (e.g., test -z
"$SCRIPT_NAME"), printing a clear error/usage message and exiting non‑zero,
before using SCRIPT_DIR, SCRIPT_NAME and the exec bash
"${SCRIPT_DIR}/${SCRIPT_NAME}" "$@" call so the behavior matches the CMD block.
Summary
Cherry-pick of
7400d43fromdevto fix Windows SessionStart hook failures.The fix:
session-start.sh→session-start(no extension) so Claude Code's.shauto-detection doesn't fire and prependbashrun-hook.cmdpolyglot wrapper to control bash invocation on Windows (tries known Git Bash paths, then PATH, then exits silently if no bash found)This avoids Claude Code's broken
.shauto-prepend, gives us control over how bash is invoked on Windows, and gracefully handles missing bash instead of erroring.Fixes
set -euo pipefailfragile on MSYSUpstream: anthropics/claude-code#14828
Test plan