Windows: three bundled plugins' hooks never run and spawn a console window — hooks.json invokes .sh by bare path

Status Open
Maintainer reply None cached
Activity 0 comments · opened Sep 20, 2026

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

  • #82893 — Windows: plugin hook commands rewritten into a cmd /d /s /c form.

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 form
plugins are expected to ship.

  • #90292 — validate-hook-schema.sh failing on plugin hook manifests; adjacent

if a lint is added (see below).

Suggested follow-up

  1. 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.

  1. 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.

View original on GitHub ↗