Repository navigation
Closed
Labels
area:corearea:toolsbugSomething isn't workingSomething isn't workinghas reproHas detailed reproduction stepsHas detailed reproduction stepsplatform:macosIssue specifically occurs on macOSIssue specifically occurs on macOSstaleIssue is inactiveIssue is inactive
Description
Hook executor doesn't set CLAUDE_PLUGIN_ROOT environment variable
Summary
Plugin hooks that use ${CLAUDE_PLUGIN_ROOT} fail with "Cannot find module" errors because the hook execution system doesn't set this environment variable before running hooks.
Reproduction
- Install plugin with hooks using
${CLAUDE_PLUGIN_ROOT}(e.g., claude-mem, claude-reflect) - Enable the plugin:
claude plugin enable claude-mem - Execute any tool or submit a prompt
- Observe hook errors in logs/TLDR
Expected Behavior
Hook executor should:
- Parse
${CLAUDE_PLUGIN_ROOT}placeholder in hook commands - Set environment variable to plugin's cache directory path
- Substitute/expand the variable before execution
Similar to how ${CLAUDE_PROJECT_DIR} is handled.
Actual Behavior
${CLAUDE_PLUGIN_ROOT}remains unexpanded- Scripts try to execute from
/scripts/...(absolute root path) - Error:
Cannot find module '/scripts/bun-runner.js'
Evidence
Plugin hook definition:
{
"PostToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" ..."
}]
}]
}Environment check:
$ env | grep CLAUDE
CLAUDE_PROJECT_DIR=/Users/.../project ✅ SET
CLAUDE_PLUGIN_ROOT= ❌ NOT SETManual verification:
# Fails without variable
$ node "/scripts/bun-runner.js"
Error: Cannot find module '/scripts/bun-runner.js'
# Works with variable set
$ export CLAUDE_PLUGIN_ROOT="$HOME/.claude/plugins/cache/thedotmack/claude-mem/9.1.1"
$ node "${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js"
✅ SuccessAffected Plugins
- claude-mem@thedotmack (v9.1.1)
- claude-reflect@claude-reflect-marketplace (v2.5.1)
- Any plugin using
${CLAUDE_PLUGIN_ROOT}in hooks
Impact
- ❌ PostToolUse observation tracking broken
- ❌ UserPromptSubmit hooks broken
- ❌ SessionStart hooks broken
⚠️ Hook errors spam logs but don't break core functionality⚠️ MCP tools from plugins still work (different mechanism)
Environment
- OS: macOS (Darwin 25.3.0)
- Claude Code: Latest as of 2026-02-09
- Plugins installed: claude-mem 9.1.1, claude-reflect 2.5.1
Additional Context
This appears to be a regression or missing feature in the hook execution system. Other environment variables like ${CLAUDE_PROJECT_DIR} work correctly, suggesting the infrastructure exists but ${CLAUDE_PLUGIN_ROOT} isn't included.
Reacted by Deividas, ACT900, Alexander Khozya, Kendall Morgan, Berk Karaal, Scott Hazlett, s.vgys, Vlad and Vitaly Schekin
Metadata
Metadata
Assignees
Labels
area:corearea:toolsbugSomething isn't workingSomething isn't workinghas reproHas detailed reproduction stepsHas detailed reproduction stepsplatform:macosIssue specifically occurs on macOSIssue specifically occurs on macOSstaleIssue is inactiveIssue is inactive
Activity
fix: add CLAUDE_PLUGIN_ROOT fallback for Stop hooks (#1215)
fix: resolve hook crashes and CLAUDE_PLUGIN_ROOT fallback (#1215, #1220…
AKhozya commentedon Feb 27, 2026
Still hitting this on every plugin update. Current workaround is a post-update sed:
Affects every plugin that uses
${CLAUDE_PLUGIN_ROOT}in hooks — including everything-claude-code (10 references in hooks.json as of v1.6.0). The variable works for MCP tool paths but not for hook commands.Would be great to get this fixed — it's the #1 source of post-update maintenance friction for plugin users.
Environment: macOS (Darwin 25.3.0), Claude Code latest, everything-claude-code 1.6.0
AKhozya commentedon Mar 2, 2026
I dug into this further using the code from PR #30024, which reveals the actual hook executor path in the compiled CLI. There are two bugs, not one:
Bug 1: Plugin context lost during hook merge. The hook executor does template substitution conditionally:
But
$(the plugin root path) isundefinedwhen plugin hooks fromhooks.jsonare merged with user hooks fromsettings.json. The plugin context (which plugin a hook belongs to) is lost during the merge, so$is never set. The command runs with the literal${CLAUDE_PLUGIN_ROOT}string, which the shell resolves to empty.Bug 2: Env var never set in spawned process. Even if template substitution worked,
CLAUDE_PLUGIN_ROOTis never added to the spawned process's environment. This breaks scripts that read it at runtime — e.g., hookify'sos.environ.get('CLAUDE_PLUGIN_ROOT')for Python imports.Why existing PRs don't fix this
${CLAUDE_PLUGIN_ROOT}inallowed-toolsfrontmatter, not in hook commandsSuggested fix
Two changes in the hook executor:
hooks.jsonloading, so the plugin root path survives the merge with user hooks:This matches how
CLAUDE_PROJECT_DIRworks (always available) and how MCP server configs handle${CLAUDE_PLUGIN_ROOT}(plugin context preserved). It would fix all affected plugins at once instead of requiring per-plugin workarounds.Impact
Every official plugin with hooks uses
${CLAUDE_PLUGIN_ROOT}: hookify (4 hooks), ralph-wiggum (1), explanatory-output-style (1), learning-output-style (1), security-guidance (1). Plus third-party plugins (everything-claude-code, claude-mem, claude-reflect). Current workaround issed -ito replace with absolute paths after every plugin update.traylorre commentedon Mar 14, 2026
Confirming on v2.1.75 (Linux/WSL2). Consistent with @AKhozya's root cause.
Additional finding: plugin hook errors are silent
Expected: Plugin hooks that fail should surface an error in the UI, the same way user-defined hooks do.
Actual: Plugin hooks that fail due to unresolved
${CLAUDE_PLUGIN_ROOT}produce no visible error. Probe testing confirms hooks are dispatched for all event types (SessionStart, PreToolUse, PostToolUse, UserPromptSubmit, Stop). They just fail silently.The error rendering path itself works. A user-defined hook that exits non-zero:
{"matcher": "Bash", "hooks": [{"type": "command", "command": "exit 1"}]}immediately shows
PreToolUse:Bash hook error. The difference is specific to plugin-origin hooks.Affects: All five
@claude-plugins-officialplugins with hooks:learning-output-style,explanatory-output-style,hookify,security-guidance,ralph-loop.Workaround
Extended @AKhozya's
sedto also set the env var for scripts that read it at runtime, and to patch bothcache/andmarketplaces/: https://gist.github.com/traylorre/0cc71f26704878751b6185f473304f54alessandropcostabr commentedon Mar 15, 2026
Confirmed on Linux — Claude Code v2.1.76 (Opus 4.6)
Platform: Ubuntu Linux 6.17.0-14-generic
Claude Code: v2.1.76
Reproduction
On Linux,
CLAUDE_PLUGIN_ROOTis empty/unset for all hook types (SessionStart, PreToolUse, PostToolUse, Stop, UserPromptSubmit). Hook commands containing${CLAUDE_PLUGIN_ROOT}expand to absolute root paths like/hooks/run-hook.cmd→ exit 127.Affected official plugins (from
claude-plugins-official)"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd" session-startpython3 ${CLAUDE_PLUGIN_ROOT}/hooks/security_reminder_hook.py${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.shpython3 ${CLAUDE_PLUGIN_ROOT}/hooks/*.py${CLAUDE_PLUGIN_ROOT}/hooks-handlers/session-start.sh${CLAUDE_PLUGIN_ROOT}/hooks-handlers/session-start.sh${CLAUDE_PLUGIN_ROOT}/scripts/check_version.shAll 7 plugins silently fail on Linux. The user sees
SessionStart:startup hook erroron every session start.Workaround
The claude-mem plugin (by @thedotmack) already implements a shell-level fallback in all its hooks. This pattern resolves the path correctly on Linux regardless of whether
CLAUDE_PLUGIN_ROOTis set:{ "type": "command", "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/cache/<plugin-path>\"; \"$_R/hooks/my-hook.sh\"" }We applied this pattern to all 7 affected plugins locally (14
hooks.jsonfiles patched across cache + marketplace directories) and the errors are resolved. All plugins function correctly after the fallback resolves the path.Suggested fix
Either:
CLAUDE_PLUGIN_ROOTon Linux (ideal)Option 1 is the root fix. Option 2 is what claude-mem already does and is a good defensive practice for any plugin.
Investigation and workaround implemented with Claude Code (Opus 4.6). Co-authored by @alessandropcostabr.
AKhozya commentedon Mar 21, 2026
Any update on it, seems quite bad and affects quite a few people
fix: add hook installer to work around CLAUDE_PLUGIN_ROOT bug (#19)
alessandropcostabr commentedon Mar 27, 2026
New finding: hooks.json is reset on every session start, invalidating all community workarounds
Platform: Ubuntu Linux 6.17.0-19-generic
Claude Code: v2.1.76
Discovery
While investigating the ralph-loop stop hook
Permission deniederror (#38686), we discovered that Claude Code overwrites pluginhooks.jsonfiles on every new session, not just onclaude plugins update.Evidence
ralph-loop/hooks/hooks.jsonto usebashprefix:Restarted Claude Code session (
ctrl+c→ reopen)Observed the file was reverted to the original unpatched command:
Our
_Rfallback,bashprefix — everything was stripped. Claude Code regenerated the hooks.json from its internal plugin definition.Impact on community workarounds
Every workaround shared in this thread assumes patches survive between sessions:
sedpost-update script → reverted on next sessionfix-plugin-root-fallbacks.sh) → reverted on next sessionThis means users must re-run their workaround scripts before every single Claude Code session, not just after
claude plugins update. In practice, this makes all file-based workarounds unreliable.enabledPluginsalso ineffectiveWe also confirmed that setting
enabledPluginstofalsedoes NOT prevent hook execution (bug #27521 still active):Result: the stop hook still fires and still produces the
Permission deniederror. The only way to stop it wasclaude plugins uninstall ralph-loop@claude-plugins-official.Summary of what DOESN'T work
hooks.json(sed/python)chmod +xon.shfilesenabledPlugins: falseclaude plugins uninstallWhat this means for the fix
The only viable path is fixing this in Claude Code itself. No user-side workaround is durable. Specifically:
CLAUDE_PLUGIN_ROOTin the spawned process environment (the original bug)enabledPluginswhen loading hooks ([BUG] Marketplace plugins with hooks cause "No such file or directory" errors for non-installed plugins #27521)bash /path/to/script.shinstead of direct exec (fixes the sandbox Permission denied issue on Linux)Without at least one of these, users on Linux cannot use any plugin with
.shhooks.Investigated with Claude Code (Opus 4.6). Co-authored by @alessandropcostabr.
fix: resolve hook crashes and CLAUDE_PLUGIN_ROOT fallback (thedotmack…
yurukusa commentedon Mar 29, 2026
Workaround: set the variable yourself in the hook:
This ensures
CLAUDE_PLUGIN_ROOTis available regardless of whether the executor sets it.alessandropcostabr commentedon Mar 29, 2026
@yurukusa The workaround above has a few issues:
Path is wrong —
CLAUDE_PLUGIN_ROOTmust be per-plugin, per-version, not a generic directory. Each plugin resolves to~/.claude/plugins/cache/<marketplace>/<plugin>/<version>. A generic fallback like$HOME/.claude/pluginswon't find the hook scripts.exit 0kills the hook — The workaround sets the variable but then exits before executing the actual hook command.Overwritten on every session — As I documented in my comment from March 27, Claude Code resets
hooks.jsonon every session start, not just onclaude plugins update. Any manual edit is lost immediately.What actually works
We maintain a script (
fix-plugin-root-fallbacks.sh) that uses the_R=fallback pattern per-plugin:This pattern:
CLAUDE_PLUGIN_ROOTfirst (for when Anthropic eventually fixes it)cache/andmarketplace/directories separatelyThe script also fixes two related issues:
+xpermissions on.shfiles after plugin updateenabledPluginsMust be re-run after every
claude plugins update(or automated via a post-update hook).Related: claude-mem plugin as a case study
We maintain the claude-mem plugin (persistent cross-session memory via semantic search). During refactoring, we fixed 3 upstream bugs in the plugin code and rebuilt it with the
_R=fallback pattern baked intohooks.json. This plugin usesUserPromptSubmit,PostToolUse,SessionStart, andStophooks — all affected by this bug.After 20 days of production use (3,100+ observations, 730+ session summaries), the
_R=fallback pattern is confirmed reliable. The plugin has zero hook failures since the fix.The real fix needs to come from Anthropic — either:
CLAUDE_PLUGIN_ROOTcorrectly in the hook executorhooks.jsonon every session start so community patches persistfix: self-discovering hooks to work around CLAUDE_PLUGIN_ROOT bug (#2…
fix: resolve hook crashes and CLAUDE_PLUGIN_ROOT fallback (thedotmack…
claude commentedon Apr 17, 2026
This issue was fixed as of version 2.1.94.
github-actions commentedon Apr 25, 2026
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.