[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

Status Open
Reported on v2.1.205
Maintainer reply None cached
Activity 1 comment · opened Jul 17, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

On macOS, plugin roots resolve under ~/Library/Application Support/…, which contains a space. Several marketplace plugins interpolate ${CLAUDE_PLUGIN_ROOT} unquoted into their hooks.json commands. The shell word-splits on the space, the hook command never finds its script, and the hook fails.

Severity depends on which hook event is wired up:

  • hookify (v0.1.0) attaches the broken command to PreToolUse. A failing PreToolUse hook rejects the tool call, so every single tool is blocked — Bash, Read, Write, Edit, even AskUserQuestion. Claude Code is completely unusable with hookify installed.
  • learning-output-style and explanatory-output-style (SessionStart), and ralph-wiggum (Stop) have the same defect. Those events don't block, so they fail silently — their hooks have simply never run on this machine, and nothing ever said so.

That's 4 affected plugins among the ones installed here. I did not audit the whole marketplace, so the real count is likely higher.

This looks like the macOS counterpart of #70150 (Windows: exec-form hook args with ${CLAUDE_PLUGIN_ROOT} lose backslashes). The shared root cause: ${CLAUDE_PLUGIN_ROOT} is not path-safe once interpolated into a shell command string, and every plugin author has to remember to quote it.

A second, independent problem made this extremely hard to diagnose — the error never names the plugin. Details under Additional Information; I'd argue it belongs to this bug rather than being a separate report.

What Should Happen?

Plugin hooks should run regardless of whether the plugin root path contains spaces. On macOS it always does.

Two levels of fix:

1. Per-plugin (immediate). Quote the interpolated path in the affected hooks.json files:

"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.py\""

Affected here: hookify (all 4 commands), learning-output-style, explanatory-output-style, ralph-wiggum.

2. Systemic (preferred). This keeps recurring — across plugins, across platforms (#70150 on Windows), and across failure modes (#39550, #43380, #59713, #65579). Every plugin author has to independently remember that ${CLAUDE_PLUGIN_ROOT} is unsafe to interpolate bare into a shell string, and on Linux the mistake is invisible, so it ships.

Options worth considering:

  • validate/lint hooks.json at plugin install or publish time and reject unquoted interpolation
  • document the quoting requirement prominently for plugin authors (superpowers already does it right — that pattern could be the documented default)
  • have the hook runner quote the substitution itself where the command is a shell string

Error Messages/Logs

Every tool call returns this. Shown for Bash; identical for Read, Write, Edit, AskUserQuestion — only the tool name changes.

PreToolUse:Bash hook error: [python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.py]: /Library/Developer/CommandLineTools/usr/bin/python3: can't open file '/Users/<user>/Library/Application': [Errno 2] No such file or directory

The truncation at "Application" is the whole story: the real path is
/Users/<user>/Library/Application Support/Claude/local-agent-mode-sessions/<uuid>/<uuid>/rpm/plugin_<id>/hooks/pretooluse.py

The variable expanded fine. The shell then split it at the space, and python3 got "/Users/<user>/Library/Application" as its filename argument.

Note that the bracketed part shows the unexpanded template, while the actual failure shows the expanded-then-split path — which is why this is a quoting bug, not an expansion bug (cf. #39550/#65579).

Steps to Reproduce

Prerequisite: macOS, where plugins stage under ~/Library/Application Support/… (path contains a space).

  1. Install the hookify plugin (v0.1.0) from the marketplace.
  2. Start Claude Code and ask it to run any tool — e.g. a trivial Bash command like echo hi.
  3. Observed: the call is rejected with the can't open file '/Users/<user>/Library/Application' error. Every subsequent tool call fails identically. Nothing works.

Confirming the cause

# locate the staged plugin (use find, not grep -r — Application Support is huge)
find ~/Library/Application\ Support/Claude -name "pretooluse.py" 2>/dev/null

# inspect the hook definitions (P = the plugin dir containing hooks/)
cat "$P/hooks/hooks.json"

All four commands contain unquoted ${CLAUDE_PLUGIN_ROOT}:

{
  "hooks": {
    "PreToolUse":       [{ "hooks": [{ "type": "command", "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.py",       "timeout": 10 }] }],
    "PostToolUse":      [{ "hooks": [{ "type": "command", "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/posttooluse.py",      "timeout": 10 }] }],
    "Stop":             [{ "hooks": [{ "type": "command", "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/stop.py",             "timeout": 10 }] }],
    "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/userpromptsubmit.py", "timeout": 10 }] }]
  }
}

Minimal demonstration of the mechanism

mkdir -p "/tmp/a b" && echo 'print("ok")' > "/tmp/a b/x.py"
R="/tmp/a b"
bash -c "python3 $R/x.py"     # fails: can't open file '/tmp/a'
bash -c "python3 \"$R/x.py\""  # ok

Verifying the fix

Quoting all four commands restores tool calls immediately, without a restart:

"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.py\""

Finding other affected plugins

grep -l '"command": "[^"]*${CLAUDE_PLUGIN_ROOT}[^"]*"' \
  ~/Library/Application\ Support/Claude/local-agent-mode-sessions/*/*/rpm/*/hooks/hooks.json

On this machine that returns 4 plugins: hookify, learning-output-style, explanatory-output-style, ralph-wiggum. Only hookify is fatal, because only hookify wires the broken command to PreToolUse; the others fail silently on SessionStart/Stop.

Claude Model

None

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.205 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Other

Additional Information

Related issues — why this is not a duplicate

GitHub's duplicate detection surfaced several CLAUDE_PLUGIN_ROOT issues. They describe a different failure mode, so I'm filing separately — please re-link if you disagree.

| Issue | Reported cause | Symptom |
|---|---|---|
| #39550 (macOS, closed) | variable expands to empty | bash: /hooks-handlers/session-start.sh: No such file |
| #43380 (closed, dup of #39550) | variable not injected | Cannot find module '/hooks/...' |
| #59713 (closed, not planned) | variable not in subprocess env | expands to empty string |
| #65579 (Windows, closed as dup) | variable left literally unexpanded | ${CLAUDE_PLUGIN_ROOT} appears verbatim in the path |
| #70150 (Windows, open) | exec-form args lose backslashes | path mangled |

This report is none of those. Here ${CLAUDE_PLUGIN_ROOT} expands correctly — the error shows the resolved prefix /Users/<user>/Library/Application — and then breaks because it is unquoted and the resolved path contains a space. Expansion works; word-splitting kills it.

Notably, #39550 concerns the same two plugins I found here (learning-output-style, explanatory-output-style) with the same command line, but reports the empty-variable symptom instead. So that command has now failed on macOS for two independent reasons.

#39550 also documents the known-good pattern: superpowers uses bash -c '"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd" session-start'quoted — and works. The fix is established; these four plugins just don't follow it.

Diagnosability — I'd argue this is part of the bug

The error names neither hookify nor plugins at all. It surfaces as a raw python3 error about /Users/<user>/Library/Application, which reads like an unrelated filesystem fault. Nothing points at a plugin, let alone which one.

Worse: the bug blocks the very tools needed to investigate it (Bash, Read, Grep) and the tools needed to apply the fix (Edit). The whole diagnosis had to happen manually in Terminal.app, outside Claude Code. It took a full session.

A hook error naming its plugin — PreToolUse hook error in plugin 'hookify' — would have turned that into about two minutes. Given that five separate issues now exist about CLAUDE_PLUGIN_ROOT failing in five different ways, plugin attribution in hook errors would pay for itself.

Version note (full disclosure)

2.1.205 is the current Homebrew cask; the npm registry showed 2.1.212 when filing. I ticked "latest version" on the basis that 2.1.205 is current for this install channel.

It shouldn't matter either way: the defect lives in hookify 0.1.0's static hooks.json, independent of the Claude Code version. Upgrading Claude Code does not re-stage or update plugins — after brew upgrade --cask claude-code, every plugin file here still carried its original install mtime (Jul 5), and hookify was still 0.1.0.

Environment

  • macOS 26.5.2 (Darwin 25.5.0), Apple silicon
  • Claude Code 2.1.205, installed via Homebrew cask
  • Claude Desktop, local agent mode
  • Plugins staged under ~/Library/Application Support/Claude/local-agent-mode-sessions/<uuid>/<uuid>/rpm/plugin_<id>/
  • hookify 0.1.0

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗