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
Summary
The Claude Code plugin hooks in
hooks/hooks.jsonassume${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.mjsand failed withMODULE_NOT_FOUND.This affects all three hook commands in the plugin, not only Stop:
Observed behavior
With
CLAUDE_PLUGIN_ROOTunset, 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:
With
CLAUDE_PLUGIN_ROOTset to the installed plugin cache path, the same hook exits cleanly: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_ROOTis empty:The same fallback was applied to
SessionStartandSessionEnd.After that, all three events passed in both cases:
Suggested fix
The ideal fix may be in Claude Code host so
CLAUDE_PLUGIN_ROOTis always injected for plugin hooks. But the plugin can also be more defensive:/scripts/..., orCLAUDE_PLUGIN_ROOTbefore invoking Node.Environment
openai-codex/codex-plugin-cc1.0.36a5c2ba53b734f3cdd8daacbd49f68f3e6c8c167/Users/junhyeong/.claude/plugins/cache/openai-codex/codex/1.0.3