Summary
Three bundled plugins register hook commands that are a bare path to a .sh
file, with no interpreter:
| plugin |
event |
command |
ralph-wiggum |
Stop |
${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh |
learning-output-style |
SessionStart |
${CLAUDE_PLUGIN_ROOT}/hooks-handlers/session-start.sh |
explanatory-output-style |
SessionStart |
${CLAUDE_PLUGIN_ROOT}/hooks-handlers/session-start.sh |
Verified against main today via the contents API, so this is current, not a
stale local checkout.
Windows has no shebang handling, so such a command is resolved through the .sh
file association. With Git for Windows installed — effectively a
prerequisite for Claude Code on Windows — that association is:
.sh = sh_auto_file
sh_auto_file = "C:\Program Files\Git\git-bash.exe" --no-cd "%L" %*
git-bash.exe is a GUI-subsystem binary. Verified from the PE optional
header rather than inferred: Subsystem = 2. It therefore creates its own
console window and does not inherit the parent's stdio handles.
C:\Program Files\Git\bin\bash.exe is Subsystem = 3 (CONSOLE) and does
inherit them. That single difference is the whole bug.
Impact
A console window opens on every invocation, titled
/usr/bin/bash --login -i <script path> — which is git-bash.exe's own
signature.
And the hook silently does nothing, by two different routes depending on
which way the script communicates:
ralph-wiggum/hooks/stop-hook.sh reads its payload from stdin
(HOOK_INPUT=$(cat)). With no inherited stdin it blocks on the new console's
terminal input until the timeout. The ralph-loop continuation never runs.
- The two output-style handlers write
hookSpecificOutput.additionalContext to
stdout. With no inherited stdout that output goes to the detached console
and is discarded, so the output-style instructions never reach the session.
Either way there is no error. The feature is simply absent, and on
macOS/Linux — where the shebang is honoured — everything works, so an author
gets no signal.
Reproduction
Windows 11, Git for Windows installed, any of the three plugins enabled.
The mechanism can be shown directly without Claude Code:
# Through the association (what happens today): blocks, returns nothing
'{"hook_event_name":"Stop"}' | cmd.exe /c 'C:\...\plugins\...\hooks\stop-hook.sh'
# With an explicit interpreter: receives the payload, returns its result
'{"hook_event_name":"Stop"}' | bash 'C:\...\plugins\...\hooks\stop-hook.sh'
Fix
Name the interpreter, and quote the expansion:
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh\""
The quotes are load-bearing independently of Windows: CLAUDE_PLUGIN_ROOT can
contain spaces on every platform, and unquoted it word-splits under /bin/sh.
This is already the convention nearly everywhere. Across the 9 marketplaces I
have installed — 76 hooks.json files, 351 hook commands — 344 prefix an
explicit interpreter and 7 do not: these three, plus two in
awslabs/agent-plugins, plus cached duplicates of the same files.
And awslabs/agent-plugins has already fixed exactly this, in exactly this
form. Their issue #146 reported the unquoted-path half (a space in
CLAUDE_PLUGIN_ROOT breaking /bin/sh), and both their hooks now read:
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/validate-template.sh\""
Two independent motivations — spaces in paths on POSIX, and the association
launcher on Windows — converging on one fix is a reasonable argument for making
it the enforced convention rather than the usual one.
I applied the same change locally to a cached copy of a bare-path hook and
confirmed both symptoms resolve: no window appears, and the hook produces a real
result for the first time on that machine.
Related, but not a duplicate
Suggested follow-up
- A CI check that every
hooks.json command begins with an explicit
interpreter. Given 344/351 already comply, this holds an existing line rather
than imposing a new one, and it would have caught all seven.
- A line in the plugin-authoring docs: a hook
command is not run through a
shell on Windows, so a bare script path is resolved by file association —
which for .sh is a GUI launcher that discards the parent's stdio. This is
not discoverable from a macOS or Linux workstation.
Environment
- Windows 11 Pro 10.0.26200
- Git for Windows (
git-bash.exe Subsystem = 2, bin\bash.exe Subsystem = 3)
- Affected plugins observed both in
plugins/cache/ and in the
claude-code-plugins marketplace checkout at bf7d404, and confirmed on
main via the contents API.
Summary
Three bundled plugins register hook commands that are a bare path to a
.shfile, with no interpreter:
ralph-wiggumStop${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.shlearning-output-styleSessionStart${CLAUDE_PLUGIN_ROOT}/hooks-handlers/session-start.shexplanatory-output-styleSessionStart${CLAUDE_PLUGIN_ROOT}/hooks-handlers/session-start.shVerified against
maintoday via the contents API, so this is current, not astale local checkout.
Windows has no shebang handling, so such a command is resolved through the
.shfile association. With Git for Windows installed — effectively a
prerequisite for Claude Code on Windows — that association is:
git-bash.exeis a GUI-subsystem binary. Verified from the PE optionalheader rather than inferred:
Subsystem = 2. It therefore creates its ownconsole window and does not inherit the parent's stdio handles.
C:\Program Files\Git\bin\bash.exeisSubsystem = 3(CONSOLE) and doesinherit them. That single difference is the whole bug.
Impact
A console window opens on every invocation, titled
/usr/bin/bash --login -i <script path>— which isgit-bash.exe's ownsignature.
And the hook silently does nothing, by two different routes depending on
which way the script communicates:
ralph-wiggum/hooks/stop-hook.shreads its payload from stdin(
HOOK_INPUT=$(cat)). With no inherited stdin it blocks on the new console'sterminal input until the timeout. The ralph-loop continuation never runs.
hookSpecificOutput.additionalContexttostdout. With no inherited stdout that output goes to the detached console
and is discarded, so the output-style instructions never reach the session.
Either way there is no error. The feature is simply absent, and on
macOS/Linux — where the shebang is honoured — everything works, so an author
gets no signal.
Reproduction
Windows 11, Git for Windows installed, any of the three plugins enabled.
The mechanism can be shown directly without Claude Code:
Fix
Name the interpreter, and quote the expansion:
The quotes are load-bearing independently of Windows:
CLAUDE_PLUGIN_ROOTcancontain spaces on every platform, and unquoted it word-splits under
/bin/sh.This is already the convention nearly everywhere. Across the 9 marketplaces I
have installed — 76
hooks.jsonfiles, 351 hook commands — 344 prefix anexplicit interpreter and 7 do not: these three, plus two in
awslabs/agent-plugins, plus cached duplicates of the same files.And
awslabs/agent-pluginshas already fixed exactly this, in exactly thisform. Their issue #146 reported the unquoted-path half (a space in
CLAUDE_PLUGIN_ROOTbreaking/bin/sh), and both their hooks now read:Two independent motivations — spaces in paths on POSIX, and the association
launcher on Windows — converging on one fix is a reasonable argument for making
it the enforced convention rather than the usual one.
I applied the same change locally to a cached copy of a bare-path hook and
confirmed both symptoms resolve: no window appears, and the hook produces a real
result for the first time on that machine.
Related, but not a duplicate
cmd /d /s /cform.Different cause (a local rewrite of the command), same family of symptom: a
shell starts interactively, reads the hook's JSON off stdin as if it were a
typed command, and the hook reports success having done nothing. That issue's
"before" snippet also shows
bash \"${CLAUDE_PLUGIN_ROOT}/...\"as the formplugins are expected to ship.
validate-hook-schema.shfailing on plugin hook manifests; adjacentif a lint is added (see below).
Suggested follow-up
hooks.jsoncommandbegins with an explicitinterpreter. Given 344/351 already comply, this holds an existing line rather
than imposing a new one, and it would have caught all seven.
commandis not run through ashell on Windows, so a bare script path is resolved by file association —
which for
.shis a GUI launcher that discards the parent's stdio. This isnot discoverable from a macOS or Linux workstation.
Environment
git-bash.exeSubsystem = 2,bin\bash.exeSubsystem = 3)plugins/cache/and in theclaude-code-pluginsmarketplace checkout atbf7d404, and confirmed onmainvia the contents API.