Skip to content

Plugin hooks fail when CLAUDE_PLUGIN_ROOT is missing on macOS #448

Description

@leejhy

Summary

The Claude Code plugin hooks in hooks/hooks.json assume ${CLAUDE_PLUGIN_ROOT} is always populated. On my macOS install it was missing for the plugin hook runner, so the Stop hook expanded to /scripts/stop-review-gate-hook.mjs and failed with MODULE_NOT_FOUND.

This affects all three hook commands in the plugin, not only Stop:

"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-lifecycle-hook.mjs\" SessionStart"
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-lifecycle-hook.mjs\" SessionEnd"
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/stop-review-gate-hook.mjs\""

Observed behavior

With CLAUDE_PLUGIN_ROOT unset, the command fails before the hook script can run:

env -u CLAUDE_PLUGIN_ROOT sh -c 'printf "{}" | node "${CLAUDE_PLUGIN_ROOT}/scripts/stop-review-gate-hook.mjs"'

Result:

Error: Cannot find module '/scripts/stop-review-gate-hook.mjs'
Node.js v25.9.0
exit=1

With CLAUDE_PLUGIN_ROOT set to the installed plugin cache path, the same hook exits cleanly:

CLAUDE_PLUGIN_ROOT=/Users/junhyeong/.claude/plugins/cache/openai-codex/codex/1.0.3 \
  sh -c 'printf "{}" | node "${CLAUDE_PLUGIN_ROOT}/scripts/stop-review-gate-hook.mjs"'
# exit=0

For the affected workspace, the review gate was disabled / default false, so the hook should have been a no-op. The failure is command path resolution before the hook logic runs, not review-gate logic.

Local workaround that fixed it

I patched both the cache copy and marketplace copy to fall back to the marketplace plugin path when CLAUDE_PLUGIN_ROOT is empty:

"command": "PLUGIN_ROOT=\"${CLAUDE_PLUGIN_ROOT:-/Users/junhyeong/.claude/plugins/marketplaces/openai-codex/plugins/codex}\"; exec node \"$PLUGIN_ROOT/scripts/stop-review-gate-hook.mjs\""

The same fallback was applied to SessionStart and SessionEnd.

After that, all three events passed in both cases:

SessionStart unset exit=0
SessionEnd unset exit=0
Stop unset exit=0
SessionStart set exit=0
SessionEnd set exit=0
Stop set exit=0

Suggested fix

The ideal fix may be in Claude Code host so CLAUDE_PLUGIN_ROOT is always injected for plugin hooks. But the plugin can also be more defensive:

  • provide a fallback root resolution path for hook commands, or
  • fail with a clear diagnostic instead of expanding to /scripts/..., or
  • route hook commands through a small resolver wrapper that checks CLAUDE_PLUGIN_ROOT before invoking Node.

Environment

  • OS: macOS 26.5.1
  • Claude Code: 2.1.203
  • Node.js: v25.9.0
  • Plugin: openai-codex / codex-plugin-cc 1.0.3
  • Marketplace commit: 6a5c2ba53b734f3cdd8daacbd49f68f3e6c8c167
  • Installed path: /Users/junhyeong/.claude/plugins/cache/openai-codex/codex/1.0.3

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions