Plugin hooks: CLAUDE_PLUGIN_ROOT not injected into hook subprocess environment

Resolved 💬 4 comments Opened May 16, 2026 by JMNetherland Closed Jun 17, 2026

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.

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗