Skip to content

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

Description

@davidcforbes

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

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

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions