Plugin hooks: CLAUDE_PLUGIN_ROOT not injected into hook subprocess environment
Summary
Plugin hooks that reference ${CLAUDE_PLUGIN_ROOT} in their command strings fail silently at every hook event because the variable is not set in the bash subprocess environment. Claude Code spawns hooks via bash -c "<command>" without injecting CLAUDE_PLUGIN_ROOT, so the variable expands to an empty string and the path becomes invalid (e.g. "/hooks/session-start.sh").
Environment
- Claude Code version: 2.1.143
- OS: Windows 11 Pro 10.0.26200
- Shell: Git Bash (C:/Program Files/Git/bin/bash.exe)
Affected plugins (confirmed)
superpowers@claude-plugins-official(v5.1.0)warp@claude-code-warp(v2.0.0)claude-plugin-dev@functional-claude(v0.6.1)gemini-cli@functional-claude(v0.6.9)
Any plugin whose hooks.json uses ${CLAUDE_PLUGIN_ROOT} in a command field is affected.
Error shown in UI
SessionStart:startup hook error
Failed with non-blocking status code: No stderr output
Repeats once per plugin hook per event (e.g. 4 errors at session start if 4 plugins have SessionStart hooks).
Root cause
hooks.json command strings like:
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd session-start"
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on-session-start.sh"
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/gemini-session-start.js"
When Claude Code spawns bash -c "<command>", CLAUDE_PLUGIN_ROOT is not present in the subprocess environment. Bash expands it to empty string, producing invalid paths.
The superpowers session-start script itself correctly checks [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] to branch output format — so the scripts are written assuming the variable will be set. The issue is in the hook spawning layer.
Workaround
Manually patch each plugin's hooks.json in the cache (~/.claude/plugins/cache/) to add a fallback using bash parameter expansion:
"command": "_R=\"${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/cache/<marketplace>/<plugin>/<version>}\"; \"$_R/hooks/script\""
This is the same pattern used by thedotmack/claude-mem, which works correctly because it includes fallback resolution. This workaround is fragile — it gets overwritten on plugin updates.
Expected behavior
CLAUDE_PLUGIN_ROOT should be set to the plugin's resolved cache path (e.g. ~/.claude/plugins/cache/claude-plugins-official/superpowers/5.1.0) in the environment before spawning any hook subprocess registered by that plugin.
Related issues
This appears related to #10936, #17088, #27886, #34713 (false hook error labels). The false-label bug compounds this one: even hooks that exit cleanly are sometimes shown as errors, making it harder to distinguish genuine failures from false positives.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗