Windows: plugin hook commands rewritten into a cmd form bash can't run — hook reports success while injecting a cmd banner

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 0 comments · opened Jul 31, 2026

Summary

On Windows, the command of already-installed plugin hooks gets rewritten into

cmd /d /s /c ""<abs path>\hooks\<wrapper>.cmd" <arg>"

while "shell": "bash" is left in place. Bash collapses the nested quotes and consumes the
backslashes, so a bare cmd launches interactively, reads the hook's JSON payload off stdin as if
it were a typed command, and returns the Windows banner.

Claude Code reports the hook as success and injects the banner as the hook's context. The hook
appears to work while doing nothing.

Environment

  • Claude Code 2.1.220 (native install, latest channel)
  • Windows 11 Pro 10.0.26300, PowerShell primary shell, Git Bash present
  • env.BASH_ENV set in settings.json
  • Affected plugins: superpowers@claude-plugins-official 6.2.0, remember@claude-plugins-official 0.8.7 / 0.8.8

What happened

Three cached hooks.json files were rewritten at the same instant, with no plugin install or
update at that time:

plugins/cache/claude-plugins-official/remember/0.8.7/hooks/hooks.json     2026-07-28 00:59:10
plugins/cache/claude-plugins-official/remember/0.8.8/hooks/hooks.json     2026-07-28 00:59:10
plugins/cache/claude-plugins-official/superpowers/6.2.0/hooks/hooks.json  2026-07-28 00:59

Before — the form plugins ship, and the form newer installs still have:

{ "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start\"", "shell": "bash" }

After the rewrite:

{ "command": "cmd /d /s /c \"\"C:\\Users\\<user>\\.claude\\plugins\\cache\\claude-plugins-official\\superpowers\\6.2.0\\hooks\\run-hook.cmd\" session-start\"",
  "shell": "bash" }

The absolute local path in the rewritten command is what rules out the marketplace as the source —
this was written on the machine.

Resulting hook output, taken from the session transcript's hook_success attachment:

Microsoft Windows [Version 10.0.26300.8935]
(c) Microsoft Corporation. All rights reserved.

C:\Users\<user>\.code>{"session_id":"...","hook_event_name":"SessionStart","source":"startup",...}

That is cmd echoing its own prompt plus the hook payload it was handed on stdin. The plugin's
actual additionalContext was never injected.

How long it ran undetected

Classifying 119 local session transcripts by what their SessionStart hook actually returned:

| Date | Banner (hook dead) | Clean |
|---|---|---|
| 2026-06-27 → 07-24 | majority | — |
| 2026-07-25 | 3 | 1 |
| 2026-07-26 | 0 | 1 |
| 2026-07-27 | 1 | 5 |
| 2026-07-28 | 13 | 0 |
| 2026-07-29 | 3 (pre-fix) | 10 (post-fix) |
| Total | 102 | 17 |

The fault is intermittent rather than one-off: clean stretches are followed by re-mangling. The
plugin's SessionStart context was missing from the large majority of sessions across roughly a
month, with no signal in the UI or logs.

Reproduce

# the rewritten command, run the way "shell": "bash" runs it:
cmd /d /s /c ""C:\path\to\plugin\hooks\run-hook.cmd" session-start" < /dev/null
# => Windows banner + interactive prompt, exit 0

# the same wrapper invoked in a bash-safe way:
echo '{"hook_event_name":"SessionStart","source":"startup"}' \
  | bash "C:/path/to/plugin/hooks/run-hook.cmd" session-start
# => {"hookSpecificOutput": {...}}   correct

Note the wrappers these plugins ship are bash/batch polyglots (: << 'CMDBLOCK'), so bash can
execute the .cmd directly — which is what makes the second form work.

Expected

Either:

  1. the rewrite emits a command that survives the shell it declares, or
  2. a hook whose output is neither valid hook JSON nor plausible context is surfaced as a failure

rather than reported as success.

Why the second half matters

The quoting bug is small. The expensive part is that a hook returning a cmd banner is still
reported as success — that is what turned a quoting mistake into a month-long silent regression,
and it would do the same for any future rewrite. It surfaced here only by reading the raw
hook_success attachments in the session .jsonl.

Workaround

Rewriting the command to bash "${CLAUDE_PLUGIN_ROOT}/hooks/session-start" fixes it, but
plugins/ is not tracked, and a later rewrite undoes it. A scheduled idempotent repair over
plugins/cache/**/hooks.json is holding for now.

Plugin versions installed after the last rewrite (remember 0.8.9, 0.9.0) came down with the
correct ${CLAUDE_PLUGIN_ROOT} form, so a fresh install produces a working hook — but per the
table above, a good install does not stay good.

Possibly related

#10373 (SessionStart hooks not working for new conversations) reports an adjacent symptom on
macOS — hooks execute but output is never injected. Different cause from this one, where the
output is injected and is simply the wrong content, but the two share a failure class worth
looking at together.

View original on GitHub ↗