hookify plugin: Import error due to CLAUDE_PLUGIN_ROOT not available as env var
Bug Description
All 4 hookify Python hook scripts (pretooluse.py, posttooluse.py, stop.py, userpromptsubmit.py) fail with:
Hookify import error: No module named 'core'
This surfaces as a systemMessage on every Stop event (and potentially other events):
"Stop says: Hookify import error: No module named 'hookify'"
Root Cause
The hook scripts rely on CLAUDE_PLUGIN_ROOT being available as an environment variable:
# hooks/pretooluse.py (and all other hook scripts)
PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
if PLUGIN_ROOT and PLUGIN_ROOT not in sys.path:
sys.path.insert(0, PLUGIN_ROOT)
However, Claude Code only interpolates ${CLAUDE_PLUGIN_ROOT} in the command string of hooks.json:
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.py"
The command string interpolation works — the correct Python script is found and executed. But inside the Python process, os.environ.get('CLAUDE_PLUGIN_ROOT') returns None because the variable was never exported as an environment variable.
When PLUGIN_ROOT is None, the if PLUGIN_ROOT and ... guard prevents sys.path from being updated, so from core.config_loader import load_rules fails with ImportError.
Suggested Fix
Add a __file__-based fallback. Each hook script lives at <plugin_root>/hooks/<script>.py, so dirname(dirname(__file__)) reliably resolves to the plugin root:
# Prefer env var, fall back to deriving from __file__
PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT') or os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if PLUGIN_ROOT not in sys.path:
sys.path.insert(0, PLUGIN_ROOT)
This needs to be applied to all 4 files:
hooks/pretooluse.pyhooks/posttooluse.pyhooks/stop.pyhooks/userpromptsubmit.py
Environment
- Claude Code (latest as of March 2026)
- macOS Darwin 25.2.0
- Python 3.x
- hookify plugin installed via Claude Code marketplace
Workaround
Users can manually patch the cached plugin files at:
~/.claude/plugins/cache/claude-plugins-official/hookify/<hash>/hooks/*.py
But these changes are lost on plugin cache refresh.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗