claude-code icon indicating copy to clipboard operation
claude-code copied to clipboard

[BUG] Fix ${CLAUDE_PLUGIN_ROOT} in command markdown OR support local project plugin installation

Open bartolli opened this issue 2 months ago • 11 comments

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.

bartolli avatar Oct 11 '25 08:10 bartolli

@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!

bartolli avatar Oct 17 '25 23:10 bartolli

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

dkmaker avatar Oct 20 '25 19:10 dkmaker

Any update? - Skills can somehow fix it but its still a bad solution imo i need a reference to the plugin folder

dkmaker avatar Oct 29 '25 10:10 dkmaker

any update: that is really a nasty bug

sdh07 avatar Nov 06 '25 16:11 sdh07

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:

  1. User runs your plugin command for the first time
  2. A bootstrap skill creates .claude/cpr.sh in their project (happens once)
  3. This script locates your plugin via the installed plugins JSON
  4. All your commands use $(.claude/cpr.sh) to reference plugin files
  5. Your plugin can now execute bundled scripts, call APIs, process files, etc.

The nice thing is it's backwards compatible - the script checks CLAUDE_PLUGIN_ROOT first, 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.

[!WARNING] This approach is not yet hardened for setup with spaces in the path to the plugin directory. Be careful about that or let it fix by claude :-)

Implementation Details

1. Create the bootstrap skill at skills/plugin-env-setup/SKILL.md:

Click to show SKILL.md
---
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:
- Commands need to execute scripts from the plugin directory
- `${CLAUDE_PLUGIN_ROOT}` is unreliable (issue #9354)
- Need a fallback mechanism to locate the plugin

## Task

### Step 1: Check if bootstrap is needed

```bash
if [ -f ".claude/cpr.sh" ]; then
    echo "✓ Plugin environment already bootstrapped"
    # Skip to Step 3
else
    echo "📦 Bootstrapping plugin environment..."
    # Continue to Step 2
fi
```

**If `.claude/cpr.sh` exists:** 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.**

```bash
mkdir -p .claude

cat > .claude/cpr.sh << 'CPREOF'
#!/bin/bash
# Claude Plugin Root (CPR) Resolver

# IMPORTANT: Change this to match your plugin's name/identifier
PLUGIN_NAME="myplugin"

# Try CLAUDE_PLUGIN_ROOT first
if [ -n "${CLAUDE_PLUGIN_ROOT}" ] && [ -d "${CLAUDE_PLUGIN_ROOT}" ]; then
    echo "${CLAUDE_PLUGIN_ROOT%/}"
    exit 0
fi

# Fallback: jq lookup
if command -v jq &> /dev/null; then
    PLUGIN_ROOT=$(jq -r '.plugins | to_entries[] | select(.key | contains("'"$PLUGIN_NAME"'")) | .value.installPath' "${HOME}/.claude/plugins/installed_plugins.json" 2>/dev/null)
    if [ -n "$PLUGIN_ROOT" ] && [ -d "$PLUGIN_ROOT" ]; then
        echo "${PLUGIN_ROOT%/}"
        exit 0
    fi
fi

# Fallback: Python lookup
PLUGIN_ROOT=$(python3 -c "
import json
try:
    with open('${HOME}/.claude/plugins/installed_plugins.json') as f:
        plugins = json.load(f)['plugins']
        for key, value in plugins.items():
            if '$PLUGIN_NAME' in key:
                print(value['installPath'].rstrip('/'))
                break
except: pass
" 2>/dev/null)

if [ -n "$PLUGIN_ROOT" ] && [ -d "$PLUGIN_ROOT" ]; then
    echo "$PLUGIN_ROOT"
    exit 0
fi

echo "Error: Could not locate $PLUGIN_NAME plugin" >&2
exit 1
CPREOF

chmod +x .claude/cpr.sh
echo "✓ Created .claude/cpr.sh"
```

---

### Step 3: Run your plugin setup

Use the resolver to locate and run your plugin scripts:

```bash
# Run setup with dependencies
bash "$(.claude/cpr.sh)/scripts/setup.sh" ${DEPENDENCIES}

# Execute Python script
"$(.claude/cpr.sh)/scripts/run.sh" analyze.py --input data.json

# Run Node.js tool
node "$(.claude/cpr.sh)/tools/parser.js" --file report.pdf
```

## Usage in Commands

```markdown
### Step 0: Environment Setup

Invoke the `myplugin:plugin-env-setup` skill to bootstrap the environment.

After setup, use `"$(.claude/cpr.sh)/scripts/..."` to reference plugin files.
```

2. Register the skill in your plugin.json (tested with top-level plugin.json instead of in .claude-plugin/plugin.json, you can also try to just not add the skill and it should be automatically be detected) :

{
  "skills": {
    "plugin-env-setup": {
      "description": "Bootstrap plugin environment",
      "path": "skills/plugin-env-setup/SKILL.md"
    }
  }
}

3. Use it in your commands - here's an example:

### Step 0: Environment Setup

Invoke the `myplugin:plugin-env-setup` skill to bootstrap the environment.

### Step 1: Execute your scripts

```bash
# Now you can reliably execute plugin scripts
"$(.claude/cpr.sh)/scripts/run.sh" analyze.py --input data.json
node "$(.claude/cpr.sh)/tools/parser.js" --file report.pdf

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 :-)

rhuss avatar Nov 27 '25 09:11 rhuss

Hey, this is nice, but this doesn't work for me. It seems the plugin.json does not understand skills this fails installation when adding it to my existing marketplace.

okbrown avatar Nov 27 '25 15:11 okbrown

Hey, this is nice, but this doesn't work for me. It seems the plugin.json does not understand skills this fails installation when adding it to my existing marketplace.

With that said despite it displaying as /<plugin>:plugin-env-setup I was able to get claude to run the skill manually. Defeats the purpose of having a smoother onboarding flow but I get it.

okbrown avatar Nov 27 '25 15:11 okbrown

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.json top-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_ROOT is available for Bash() 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 ....

rhuss avatar Nov 27 '25 17:11 rhuss

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):

/skills
  /resources
    /docs
      adr_guide.md
      spec_guide.md
    SKILL.md
---
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:

```
Resources folder: {resources_folder}
```

Tool usage log:

> /your-plugin-name:design is running… specs/2025-12-04-jwt-integration/README.md 

⏺ I'll execute the design command for the JWT integration specification.

  First, let me validate the input and read the specification:

⏺ Read(specs/2025-12-04-jwt-integration/README.md)
  ⎿  Read 147 lines

⏺ Search(pattern: "docs/adr/[0-9][0-9][0-9][0-9]-*.md")
  ⎿  Found 0 files 

⏺ Skill(your-plugin-name:resources)

⏺ Now I have all the information needed.

  - Spec path: specs/2025-12-04-jwt-integration/README.md
  - Spec directory: specs/2025-12-04-jwt-integration
  - ADR directory: docs/adr
  - ADR number: 0001 (no existing ADRs found)
  - Resources folder: /Users/jkappers/.claude/plugins/marketplaces/your-plugin-name/skills/resources

  Let me invoke the technical-designer agent:

⏺ your-plugin-name:technical-designer(Create technical design for JWT integration)
  Read(specs/2025-12-04-jwt-integration/README.md)
  Read(~/.claude/plugins/marketplaces/your-plugin-name/skills/resources/docs/spec_guide.md)
  Read(~/.claude/plugins/marketplaces/your-plugin-name/skills/resources/docs/adr_guide.md)
  Read(~/.claude/plugins/marketplaces/your-plugin-name/skills/resources/templates/adr-template.md)

jkappers avatar Dec 04 '25 22:12 jkappers

I ran a test with v2.0.41 when both env variables were added to bash command processing:

CHANGELOG v2.0.41:

  • Slash Commands: Added CLAUDE_PROJECT_DIR and CLAUDE_PLUGIN_ROOT env vars to bash command processing

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/[email protected] for reproducibility.

here's the test slash command:

---
allowed-tools: Bash(node:*)
description: Test CLAUDE_PROJECT_DIR and CLAUDE_PLUGIN_ROOT env vars to bash command processing
model: haiku
---

## This is a test command

CLAUDE_PLUGIN_ROOT = !`node -e "console.log(process.env.CLAUDE_PLUGIN_ROOT)"`
CLAUDE_PROJECT_DIR = !`node -e "console.log(process.env.CLAUDE_PROJECT_DIR)"`

## Your task

Answer the question: What's the value of CLAUDE_PLUGIN_ROOT & CLAUDE_PROJECT_DIR env variables?

Hope this helps track down the regression in setting env vars upon Node init.

bartolli avatar Dec 06 '25 16:12 bartolli