From 390bc07abbae22f6bcbeaea98a0c7216dd9d5281 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Thu, 16 Jul 2026 00:13:35 +0000 Subject: [PATCH] fix(hooks): dispatch the SessionStart hook via Git Bash on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SessionStart command string starts with a quoted path, which breaks both Windows shells Claude Code may hand it to: PowerShell parses the leading quoted string as an expression and dies on the next bareword ('Unexpected token session-start', #1751), and cmd.exe's /c quote rule drops the outer quotes when the path contains a metacharacter, so a profile dir like C:\Users\Name(External) truncates the command at the '(' (#1918). Either way the bootstrap silently never loads. Declare shell: "bash" on the hook. Claude Code >= 2.1.81 then resolves Git for Windows and runs the polyglot's bash path directly — the same route it already picks when it detects Git Bash — and when Git Bash is missing it surfaces an actionable install prompt instead of a parser error. Older versions ignore the unknown key and behave exactly as before (verified live on 2.0.77 and 2.1.80). Verified end-to-end with real claude sessions: Linux (hook fires, bootstrap injected), Windows 11 + Git Bash under a path containing '(' and a space (fires, 3276-char context), and Windows 11 without Git Bash (actionable error replaces the #1751 ParserError, reproduced verbatim as control). Fixes #1751 Fixes #1918 --- hooks/hooks.json | 1 + tests/hooks/test-session-start.sh | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/hooks/hooks.json b/hooks/hooks.json index 79d8cee3..67975137 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -7,6 +7,7 @@ { "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start", + "shell": "bash", "async": false } ] diff --git a/tests/hooks/test-session-start.sh b/tests/hooks/test-session-start.sh index b027f3c6..c1508290 100755 --- a/tests/hooks/test-session-start.sh +++ b/tests/hooks/test-session-start.sh @@ -143,6 +143,27 @@ for (const forbiddenText of forbiddenTexts) { echo "SessionStart hook output tests" +# Registration shape: the hook must declare shell:"bash" so Claude Code on +# Windows dispatches via Git Bash (or fails with an actionable error) instead +# of PowerShell/cmd.exe, whose parsers break on the quoted command string +# (PowerShell ParserError; cmd.exe quote-stripping on paths with metacharacters). +if node -e ' +const hooks = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); +const entry = hooks.hooks.SessionStart[0].hooks[0]; +if (entry.shell !== "bash") { + console.error(`SessionStart hook shell is ${JSON.stringify(entry.shell)}, expected "bash"`); + process.exit(1); +} +if (!/run-hook\.cmd" session-start$/.test(entry.command)) { + console.error(`unexpected SessionStart command shape: ${entry.command}`); + process.exit(1); +} +' "$REPO_ROOT/hooks/hooks.json"; then + pass "hooks.json registers SessionStart with shell:bash dispatch" +else + fail "hooks.json registers SessionStart with shell:bash dispatch" +fi + claude_home="$(make_home claude-code)" assert_command_output \ "Claude Code emits nested SessionStart additionalContext" \