Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save gofullthrottle/dca4f8fd46a32fb2a3c4561b6c7d137f to your computer and use it in GitHub Desktop.

Select an option

Save gofullthrottle/dca4f8fd46a32fb2a3c4561b6c7d137f to your computer and use it in GitHub Desktop.
Workaround for ${CLAUDE_PLUGIN_ROOT} not expanding in Claude Code plugin hooks (issue #42564) — three approaches with trade-offs.

Workaround: ${CLAUDE_PLUGIN_ROOT} not expanding in Claude Code plugin hooks

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.2SessionStart, SessionEnd, Stop hooks all broken
  • claude-code-plugins/ralph-wiggum — plugin author shipped hardcoded paths to work around it
  • claude-plugins-official/ralph-loop — same

Symptom

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.

Workaround 1 — Hardcoded absolute path

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

Workaround 2 — Shell default expansion

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

Workaround 3 — Inline resolver

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

The real fix

Claude Code harness should either:

  1. Set CLAUDE_PLUGIN_ROOT in the env when invoking plugin hook commands, or
  2. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment