Skip to content

Comments

fix: restore polyglot wrapper for Windows hook compatibility (4.3.1)#523

Merged
obra merged 2 commits intomainfrom
fix/windows-hooks-4.3.1
Feb 21, 2026
Merged

fix: restore polyglot wrapper for Windows hook compatibility (4.3.1)#523
obra merged 2 commits intomainfrom
fix/windows-hooks-4.3.1

Conversation

@obra
Copy link
Owner

@obra obra commented Feb 21, 2026

Summary

Cherry-pick of 7400d43 from dev to fix Windows SessionStart hook failures.

The fix:

  • Rename session-start.shsession-start (no extension) so Claude Code's .sh auto-detection 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.

Fixes

Upstream: anthropics/claude-code#14828

Test plan

  • Verify hook loads on macOS/Linux (no regression)
  • Verify hook loads on Windows with Git Bash
  • Verify hook loads on Windows without WSL
  • Verify no visible terminal window spawns on Windows

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
@coderabbitai
Copy link

coderabbitai bot commented Feb 21, 2026

📝 Walkthrough

Walkthrough

SessionStart 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

Cohort / File(s) Summary
Git attributes
\.gitattributes
Added rule hooks/session-start text eol=lf to enforce LF line endings for the session-start hook.
Hooks metadata
hooks/hooks.json
Replaced direct shell script invocation (session-start.sh) with a CMD wrapper invocation ('${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd' session-start).
Windows/Cross-platform wrapper
hooks/run-hook.cmd
Rewrote batch wrapper to search multiple standard Git Bash locations and PATH for bash.exe, invoke the target script with found Bash (preserve exit code), and silently exit with code 0 if Bash is not found; Unix/exec path adjusted to run script via bash with script-dir resolution.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Poem

A rabbit peeks where hooks were stuck in place,
It hops and finds Bash in many a space.
With run-hook.cmd it nudges scripts to run,
Quiet if Bash hides — no mischief done.
Hooray — hooks hop smooth from Windows to Unix! 🐇

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: restoring a polyglot wrapper for Windows hook compatibility, which is the central focus of the changeset across all modified files.
Description check ✅ Passed The description is directly related to the changeset, explaining the rationale for changes, listing the specific fixes addressed, and providing a detailed test plan.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/windows-hooks-4.3.1

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +18 to +34
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%
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested 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%
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.

Comment on lines +42 to +46
# 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}" "$@"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested 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}" "$@"
# 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant