Repository navigation
fix(plugins): run ralph-wiggum and output-style .sh hooks through bash with a quoted path - #95698
Open
claude[bot] wants to merge 1 commit into
Open
claude[bot] wants to merge 1 commit into
claude[bot] wants to merge 1 commit into
Conversation
…h with a quoted path
The Stop hook in ralph-wiggum and the SessionStart hooks in
learning-output-style and explanatory-output-style gave a bare, unquoted
"${CLAUDE_PLUGIN_ROOT}/....sh" as the hook command. Shell-form hook
commands are expanded by the shell from the CLAUDE_PLUGIN_ROOT env var,
so a plugin root containing a space word-splits and the hook exits 127
("not found") with no visible effect; the bare form also relies on the
script's exec bit and, on Windows, on the CLI prepending an interpreter.
Invoke the scripts as `bash "${CLAUDE_PLUGIN_ROOT}/....sh"`, the form
security-guidance already uses, and bump the three plugins to 1.0.1 so
existing installs pick the change up on update (the plugin.json version
is what the updater compares).
Refs #95673, #78490.
This was referenced Sep 20, 2026
5 of 6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #95673. Also fixes the ralph-wiggum / output-style half of #78490.
What changes
Three bundled plugins registered their hook as a bare, unquoted script path:
ralph-wiggumStop${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.shbash "${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh"learning-output-styleSessionStart${CLAUDE_PLUGIN_ROOT}/hooks-handlers/session-start.shbash "${CLAUDE_PLUGIN_ROOT}/hooks-handlers/session-start.sh"explanatory-output-styleSessionStartThis is the form
security-guidancealready uses (bash "${CLAUDE_PLUGIN_ROOT}/hooks/sg-python.sh" …). Each plugin'sversiongoes 1.0.0 → 1.0.1 inplugin.jsonand in themarketplace.jsonentry, because the updater compares theplugin.jsonversion: without the bump, existing installs stay on the cached 1.0.0 copy and never receive the newhooks.json.Why
Shell-form hook commands are handed to a shell (
sh -con macOS/Linux, Git Bash on Windows) withCLAUDE_PLUGIN_ROOTexported, and the shell expands the variable. Three consequences of the bare form:not found. For these three hooks that is silent: the output styles contribute no context and the ralph loop never continues. ([BUG] Unquoted ${CLAUDE_PLUGIN_ROOT} in plugin hooks.json breaks all hooks on macOS (space in path); hookify's PreToolUse hook blocks every tool call #78490; a home directory likeC:\Users\Jane Doeor/Users/jane doeis enough.)+xturns the hook intoPermission denied(exit 126).bash "<path>"reads the script as data and does not care.bashto a hook command whose first word is a.shfile when it runs hooks through Git Bash (see the 2.1.161 changelog entry for Windows: SessionStart hooks that invoke bash fail (exit 127, bash: command not found) #63828), so on an up‑to‑date CLI with Git Bash detected these hooks already get an interpreter — but still unquoted, so point 1 applies, and hosts on older CLIs get the file-association behaviour Windows: three bundled plugins' hooks never run and spawn a console window — hooks.json invokes .sh by bare path #95673 describes. Naming the interpreter in the plugin removes the dependency either way; the CLI leaves a command that already starts withbashuntouched.What could break, and for whom
stop-hook.shis#!/bin/bash, the session-start handlers are#!/usr/bin/env bash).Considered and deliberately not done
"command": "bash", "args": ["${CLAUDE_PLUGIN_ROOT}/…"]) avoids quoting entirely, but on Windows a barebashspawned without a shell can resolve to WSL'sbash.exerather than Git Bash, and exec form needs a newer CLI than shell form. Shell form with quotes matches the existing convention."shell": "bash"on each hook would turn the no-Git-Bash Windows case into an explicit "requires bash" error. Left out to keep parity withsecurity-guidance; happy to add if preferred.python3 ${CLAUDE_PLUGIN_ROOT}/…has the same word-splitting problem (and blocks every tool call when it hits). It already has two open PRs, fix: quote ${CLAUDE_PLUGIN_ROOT} in plugin hook commands #79644 and fix(plugins): quote ${CLAUDE_PLUGIN_ROOT} in hook commands, prefix hookify examples #81670, which also quote the three files here but keep the bare-path form; this PR supersedes their hunks for these three files only./ralph-loop's"${CLAUDE_PLUGIN_ROOT}/scripts/setup-ralph-loop.sh"runs through the Bash tool, is already quoted, and is the subject of fix(ralph-wiggum): stop parsing /ralph-loop prompt text as shell code #80495; not touched.ralph-wiggum'sstop-hook.shneedsjq, which Git for Windows does not ship, so on Windows the Stop hook now launches correctly but still fails atjqwhen a loop is active. Separate problem; noted as a follow-up.hooks.jsoncommand names an interpreter, and a plugin-docs note — are not in this PR. The hooks reference already says shell-form commands run through Git Bash on Windows; the lint is a reasonable follow-up and would be the regression guard this repo currently lacks forplugins/*/hooks/hooks.json.Verification
claude plugin validatepasses on the marketplace root and on each of the three plugins (the only warning is the pre-existingsecurity-guidanceentry/plugin.json version mismatch).plugin root with space:CLAUDE_PLUGIN_ROOT=".../plugin root with space/ralph-wiggum" sh -c '${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh'→ exit 127,sh: 1: .../plugin: not found(same for both session-start hooks)sh -c 'bash "${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh"'→ exit 0; the session-start hooks emit validhookSpecificOutputJSON (1018 and 3034 chars ofadditionalContext); with an active.claude/ralph-loop.local.mdthe Stop hook reads stdin and returns{"decision":"block",…,"🔄 Ralph iteration 2 …"}.chmod -x stop-hook.sh: still exit 0; the quoted-but-bare form from fix: quote ${CLAUDE_PLUGIN_ROOT} in plugin hook commands #79644/fix(plugins): quote ${CLAUDE_PLUGIN_ROOT} in hook commands, prefix hookify examples #81670 gives exit 126Permission deniedin that case.--plugin-dirfrom the spaced path in-pmode: the debug log showsHook SessionStart (bash "${CLAUDE_PLUGIN_ROOT}/hooks-handlers/session-start.sh") provided additionalContext (1018 chars)and the model echoed the explanatory instructions; the unmodified copy loggedHook SessionStart:startup (SessionStart) error: /bin/sh: 1: .../main: not foundand the model answeredNONE. With ralph-wiggum and an active loop file, the log showsHook Stop (bash "${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh") returned permissionDecision: denyand the loop advanced to iteration 2.plugins/plugin-dev/.../validate-hook-schema.shwas tried and fails identically onmainand on this branch (jq: Cannot index string with number— it does not understand the plugin wrapper format, [BUG] validate-hook-schema.sh fails on plugin hook manifests and non-tool hooks (Fix included) #90292), so it is not a usable check here.plugins/*/hooks/hooks.json, so there is no automated regression test in the diff; the interpreter-prefix lint above would be it.Generated by Claude Code