Claude Code plugin hooks defined in hooks.json that reference ${CLAUDE_PLUGIN_ROOT} silently fail because the variable is not expanded by the harness and not set in the shell environment when the hook command is invoked. Tracking issue: anthropics/claude-code#42564.
Verified affected plugins as of 2026-04-28:
openai-codex/codex@1.0.2—SessionStart,SessionEnd,Stophooks all brokenclaude-code-plugins/ralph-wiggum— plugin author shipped hardcoded paths to work around itclaude-plugins-official/ralph-loop— same
Failing example from codex/1.0.2/hooks/hooks.json:
{
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/stop-review-gate-hook.mjs\""
}The shell receives the literal ${CLAUDE_PLUGIN_ROOT} (or empty string after expansion), tries to execute node "/scripts/stop-review-gate-hook.mjs", and fails with Cannot find module on every Stop event.
Brittle but always works.
{
"command": "node \"/Users/<you>/.claude/plugins/cache/openai-codex/codex/1.0.2/scripts/stop-review-gate-hook.mjs\""
}| ✅ | Always works |
| ❌ | Breaks on plugin version bump |
| ❌ | Not portable across machines |
Cleanest if you accept a hardcoded fallback. Uses the env var if set, falls back when not.
{
"command": "node \"${CLAUDE_PLUGIN_ROOT:-/Users/<you>/.claude/plugins/cache/<owner>/<plugin>/<version>}/scripts/foo.mjs\""
}| ✅ | Self-healing if Claude Code starts setting the env var |
| ✅ | Falls back when env var is missing |
| ❌ | Plugin author has to add it to every hook |
| ❌ | Fallback path still hardcoded |
Most robust. Plugin's hook resolves its own root at execution time.
{
"command": "node -e \"const p=require('path'),fs=require('fs');const tries=[process.env.CLAUDE_PLUGIN_ROOT,p.join(process.env.HOME,'.claude/plugins/cache/<owner>/<plugin>')];const root=tries.find(t=>t&&fs.existsSync(t));require(p.join(root,'scripts/foo.mjs'))\""
}| ✅ | Survives plugin version bumps (resolves at runtime) |
| ✅ | Works regardless of whether env var is set |
| ❌ | Awkward inline JS |
| ❌ | Plugin author still writes it per-hook |
Claude Code harness should either:
- Set
CLAUDE_PLUGIN_ROOTin the env when invoking plugin hook commands, or - Expand
${CLAUDE_PLUGIN_ROOT}in the command string before shelling out.
Either fix lets plugin authors write the documented pattern and have it work. See anthropics/claude-code#42564.