[BUG] Fix ${CLAUDE_PLUGIN_ROOT} in command markdown OR support local project plugin installation
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Currently, the ${CLAUDE_PLUGIN_ROOT} environment variable, which is documented as the way for plugins to reference their own files, only works in JSON configurations (hooks, MCP servers) but not in command markdown files. This makes it impossible for /plugin commands to execute bundled scripts.
What Should Happen?
Support local plugin installation
/plugin install my-plugin@marketplace --local
- Install to .claude/plugins/ in the project directory
- Commands can use project-relative paths:
node .claude/plugins/codanna-plugin/scripts/helper.js - Consistent with existing .claude/commands/ and .claude/agents/ patterns
Error Messages/Logs
▐▛███▜▌ Claude Code v2.0.22
▝▜█████▛▘ Sonnet 4.5 · Claude Max
▘▘ ▝▝ /Users/bartolli/Projects/codanna
> /test is running…
⎿ Allowed 1 tools for this command
⏺ I'll execute the command to check the CLAUDE_PLUGIN_ROOT environment variable and run the requested command.
⏺ Bash(echo "CLAUDE_PLUGIN_ROOT=$CLAUDE_PLUGIN_ROOT")
⎿ CLAUDE_PLUGIN_ROOT=
⏺ The CLAUDE_PLUGIN_ROOT environment variable is not set (empty/undefined).
Since the variable is undefined, I cannot execute the command:
node ${CLAUDE_PLUGIN_ROOT}/scripts/context-provider.js find "Exit codes" --limit=1
This would expand to:
node /scripts/context-provider.js find "Exit codes" --limit=1
Which would fail because there's no valid path.
The value of CLAUDE_PLUGIN_ROOT is: undefined/empty
node:internal/modules/cjs/loader:1386
throw err;
^
Error: Cannot find module '/scripts/context-provider.js'
at Function._resolveFilename (node:internal/modules/cjs/loader:1383:15)
at defaultResolveImpl (node:internal/modules/cjs/loader:1025:19)
at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1030:22)
at Function._load (node:internal/modules/cjs/loader:1192:37)
at TracingChannel.traceSync (node:diagnostics_channel:322:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:237:24)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
at node:internal/main/run_main_module:36:49 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
Node.js v22.20.0
Steps to Reproduce
Custom command installed via /plugin
description: Analyze code
Execute: node ${CLAUDE_PLUGIN_ROOT}/scripts/analyzer.js
Results in:
Error: Cannot find module '/scripts/analyzer.js'
Because ${CLAUDE_PLUGIN_ROOT} expands to nothing, making it impossible for plugin commands to run bundled scripts that analyze project files, parse JSON, call APIs, or any other programmatic task.
Claude Model
None
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
2.0.14
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
Many plugins need helper scripts for:
- Complex data processing (JSON parsing, formatting)
- API integrations
- Code analysis tools
- Build system integrations
Without this fix, plugin commands are limited to simple prompt engineering without any computational capabilities.
25 Comments
@bcherny Hey Boris, could you please escalate this?
Currently, the only workaround is manually adding the plugin root directory path as an environment variable in settings.local.json, which doesn't scale well across users or installations.
It would be really useful if plugin skills, slash commands and subagents could natively access plugin scripts via a standardized environment variable (like CLAUDE_PLUGIN_ROOT and CLAUDE_PROJECT_DIR). This would enable more creative automation workflows without requiring Claude to handle trivial script orchestration or computational tasks!
Thanks a lot!
Well the local in the repo can work but it shouldn't actually just be in the ~/.claude folder where I presume the scripts are located but the Claude Project env should be available so the scripts can do a cd to the folder by resolving that
Or both options - I prefer the update to follow the plugin so if you have many repos and expect a new version then it will be a mess handling
There is several combinations though and many usecases to cover but it's indeed critical somehow to get it solved
Any update? - Skills can somehow fix it but its still a bad solution imo i need a reference to the plugin folder
any update: that is really a nasty bug
I ran into this same issue while building a plugin that needs to execute Python scripts. Since
${CLAUDE_PLUGIN_ROOT}isn't reliably available in command markdown, I put together a workaround that's been working great in production. Thought I'd share the implementation in case it helps other plugin developers.The idea is to create a small resolver script that figures out where your plugin lives by reading
~/.claude/plugins/installed_plugins.json. On first use, your plugin bootstraps this script into the user's project at.claude/cpr.sh. After that, all your commands just use$(.claude/cpr.sh)instead of${CLAUDE_PLUGIN_ROOT}.How it flows:
.claude/cpr.shin their project (happens once)$(.claude/cpr.sh)to reference plugin filesThe nice thing is it's backwards compatible - the script checks
CLAUDE_PLUGIN_ROOTfirst, so when the bug gets fixed, it'll automatically use the official variable. And if you want to remove the workaround entirely, it's just a simple find/replace.Implementation Details
1. Create the bootstrap skill at
skills/plugin-env-setup/SKILL.md:<details>
<summary>Click to show SKILL.md</summary>
````markdown
---
description: Bootstrap plugin environment by creating .claude/cpr.sh resolver script. Use at the start of any command that needs to execute plugin scripts.
---
Plugin Environment Setup Skill
Creates
.claude/cpr.sh(Claude Plugin Root resolver) if it doesn't exist.Purpose
Solves the bootstrap problem where:
${CLAUDE_PLUGIN_ROOT}is unreliable (issue #9354)Task
Step 1: Check if bootstrap is needed
If
.claude/cpr.shexists: Skip to Step 3.If not: Continue to Step 2.
---
Step 2: Create .claude/cpr.sh (only if needed)
ONLY execute if Step 1 found cpr.sh missing.
---
Step 3: Run your plugin setup
Use the resolver to locate and run your plugin scripts:
Usage in Commands
````
</details>
2. Register the skill in your
plugin.json(tested with top-levelplugin.jsoninstead of in.claude-plugin/plugin.json, you can also try to just _not_ add the skill and it should be automatically be detected) :3. Use it in your commands - here's an example:
I'm using this in a production plugin with 10 commands that execute Python scripts for text analysis, PDF processing, and code validation. It's been solid across multiple projects and machines. The bootstrap happens once per project, then everything just works.
Obviously this is a workaround and native
${CLAUDE_PLUGIN_ROOT}support would be ideal, but this gets plugins fully functional in the meantime. Hope it helps! 🚀Assisted-By: 🤖 Claude Code (but with a real human behind :-)
Hey, this is nice, but this doesn't work for me. It seems the plugin.json does not understand
skillsthis fails installation when adding it to my existing marketplace.With that said despite it displaying as
/<plugin>:plugin-env-setupI was able to get claude to run the skill manually.Defeats the purpose of having a smoother onboarding flow but I get it.
You could leave out the skills part (it works for me in the latest claude versions), then you get some auto-detection which should work as well. Actually I forgot to mention that I usually put
plugin.jsontop-level in the plugin directory, I hate all those .dot files that you need to commit (they should be only for runtime information that is not committed to git). So, could be that the relative path only works when top-level in the plugin directory, but as said, you can just ommit this (should also work but even less tested :-) . I've updated that info in the comment above.Said that, I really would also love to not have to apply this worakround, and strangely sometimes it works, sometimes not. E.g. I _think_
$CLAUDE_PLUGIN_ROOTis available forBash()tooling in _skills_ but not in _commands_. So another workaround that you can try is to create a skill instead of a command and then have a command that just says: Call that skill. worth testing ....EDIT: Simplified significantly
Thanks @rhuss for the idea to let skills resolve paths.
In case it's helpful, it was enough for me to simply create a skill and add an instruction in the skill metadata for the main agent to use the skill to resolve resource paths before launching any of my plugin subagents and providing the path to the subagent prompt.
https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview#level-1-metadata-always-loaded
Directory structure (partial):
````
---
name: resources
description: Returns plugin resource path. Invoke BEFORE launching any your-plugin-name agents and pass the path to them.
---
Resource Path
This skill's folder location is the
{resources_folder}. Pass it to agents in prompts:````
Tool usage log:
I ran a test with v2.0.41 when both env variables were added to bash command processing:
CHANGELOG v2.0.41:
v2.0.41 output:
CLAUDE_PROJECT_DIR = /Users/bartolli/Projects/sdk-rust
CLAUDE_PLUGIN_ROOT = undefined
v2.0.60 output (current):
CLAUDE_PROJECT_DIR = undefined
CLAUDE_PLUGIN_ROOT = undefined
Regression confirmed. CLAUDE_PROJECT_DIR worked in v2.0.41 and broke somewhere between 2.0.41 - 2.0.60.
Tested via npx @anthropic-ai/claude-code@2.0.41 for reproducibility.
here's the test slash command:
Hope this helps track down the regression in setting env vars upon Node init.
v2.0.64 seems to have broken CLAUDE_PLUGIN_ROOT for hooks JSON as well. A plugin hook that was working in v2.0.61 is broken in v2.0.64 because CLAUDE_PLUGIN_ROOT is resolving to ~/.claude/plugins/cache/\<marketplace>/\<plugin>/unknown/.
~~Any updates? same issue on 2.0.76 with plugin slash commands.~~
I noticed that when a skill launches, Claude receives a message like the following:
After Claude itself used this information to recover from a "script not found" error during skill execution, I added instructions to my skill to read this message and save it off for later use.
Seems formalizing the "Skill Base Directory" as a value available for skill execution is a reasonable next step!
Looks like this is an issue even with the example skills that Anthropic publishes, btw:
https://github.com/anthropics/skills/blob/69c0b1a0674149f27b61b2635f935524b6add202/skills/docx/ooxml.md?plain=1#L276
This explains why my ralph hooks have stopped working, seemingly since I upgraded to 2.1.*. Claude Code recommended I let people know that it's killing the official ralph-loop plugin...
<img width="1324" height="491" alt="Image" src="https://github.com/user-attachments/assets/ae37d096-02e3-4fa6-9df5-1d691afab874" />
My relatively simple work-around:
Add this hook to my plugin:
then in my Markdown files I just do stuff like:
or:
And in actual scripts, instead of using CLAUDE_PLUGIN_ROOT (empty) or MY_PLUGIN_ROOT (not an actual env var),
I have code like:
For the Markdown, the LLM is smart enough to just substitute the value passed into the system prompt via the hook.
And the script detects its own location and finds the plugin root.
Seems to be working, though you need to remember to tell Claude to pass the variable to any of your custom agents that need it.
Marketplace Plugin Developer Perspective
We maintain a marketplace plugin suite (wiz + hud) with 25+ agents, 23 commands, and 11 skills that rely heavily on CLI scripts for interactive workflows (journal management, story tracking, code review orchestration).
The core problem
Our skills need to instruct Claude to run bundled Node.js scripts:
Since
${CLAUDE_PLUGIN_ROOT}doesn't resolve in skill/command markdown, we inject a text pointer via SessionStart hook:Claude sees
WIZ_SCRIPTS: ~/.claude/plugins/cache/marketplace/wiz/3.2.0/scriptsas text and uses it when composing Bash commands. This works but:!backtick — dynamic context (!node $WIZ_SCRIPTS/...`) fails because$WIZ_SCRIPTS` isn't a real env var at execution time.What would fix this
In priority order:
CLAUDE_PLUGIN_ROOTshould be a real env var available in!backtick commands and Bash tool calls triggered from skill/command markdown (this worked in v2.0.41 per @bartolli's report).CLAUDE_PLUGIN_ROOTto skill frontmatter variable expansion — so${CLAUDE_PLUGIN_ROOT}in SKILL.md body gets replaced at load time, just like$ARGUMENTSdoes.plugin.jsonto declare patterns that don't trigger auth prompts, e.g.:``
json
``{
"permissions": {
"bash": ["node ${CLAUDE_PLUGIN_ROOT}/scripts/*"]
}
}
Item 1 alone would unblock the entire plugin ecosystem. Items 2-3 would make it delightful.
Workaround we're using
SessionStart hook injects path as text → Claude reads it → Claude composes Bash commands with that path → user approves each Bash call. It works, but it's 3 layers of indirection for what should be
node ${CLAUDE_PLUGIN_ROOT}/scripts/foo.js.+1
For skills, SKILL.md gets loaded correctly -- so Claude obviously knows where it lives. How hard is it to wire in
$CLAUDE_PLUGIN_ROOT?2.1.69 released today - includes this morsel:
Not strictly for "plugins" but I suspect solves this problem for non-trivial skills.
This is great, I noticed my skills were more easily finding their scripts. CLAUDE_PLUGIN_ROOT really needs to be set though too.
This fix breaks using skills across many agents. Its a Claude only fix that requires modifying reusuable skills breaking them for other agents.
Yeah, I think this is only an issue when using a Claude Code "plugin" skill, and is not an issue if you merely copy-paste the skill into your project
.claude/directorySupporting "plugin" skills would have to be added to this standard, I suppose?
https://agentskills.io/skill-creation/using-scripts#referencing-scripts-from-skill-md
Do I understand this correctly?
In my case, we put them in ~/.claude/skills not a local project skills directory.
They are placed there and updated by Skillshare; using a symlink.
They work flawlessly with OpenCode.
The
${CLAUDE_PLUGIN_ROOT}variable not expanding in command markdown is a known limitation. Here's a hook-based workaround:Until native local install is supported, you can manually set up project-local plugins:
The simplest workaround for commands that need to run bundled scripts:
oof.. i just banged my head against the wall trying to debug what was going on, not realizing this was a bug for
${CLAUDE_PLUGIN_ROOT}.