CLAUDE_PLUGIN_ROOT unset in agent Bash env, and still source-dir in hooks, for string-source local marketplace plugins (2.1.265)
Bug Description
Two related CLAUDE_PLUGIN_ROOT defects, measured on 2.1.265 with a local-directory marketplace whose plugin entry uses a string source ("source": "./"):
CLAUDE_PLUGIN_ROOTis absent entirely from the agent's Bash tool environment — not wrong, unset. This is a regression against the behaviour recorded in #38699, whose table documented the agent Bash environment as the one context that correctly resolvedinstallPath.- Hook
${CLAUDE_PLUGIN_ROOT}still resolves to the marketplace source directory, not the install directory — i.e. #38699's original symptom, reproducing after its 2.1.94 fix, for a string-source plugin entry.
#38699 is closed and locked, so per the lock message this is a new issue referencing it. Related: #9354 (${CLAUDE_PLUGIN_ROOT} unavailable in command markdown), #39156.
Steps to Reproduce
- Create a local marketplace whose plugin entry uses a string source:
// .claude-plugin/marketplace.json
{
"name": "plugroot-mkt",
"owner": { "name": "example" },
"plugins": [{ "name": "plugroot-probe", "source": "./", "description": "probe" }]
}
- Give the plugin a skill that instructs the agent to run, in a Bash tool call:
printenv | sort | awk 'BEGIN{IGNORECASE=1} /PLUGIN/'
- Give the plugin a
hooks/hooks.jsonwith aPostToolUsematcher onBash:
printf 'HOOK_CLAUDE_PLUGIN_ROOT=[%s]\n' "${CLAUDE_PLUGIN_ROOT}" >> /tmp/plugroot-hook-observed.log
claude plugin marketplace add <dir>thenclaude plugin install plugroot-probe@plugroot-mkt.- Run the skill and inspect both outputs.
Actual Behaviour
Finding 1 — absent from the agent's Bash environment. The PLUGIN-filtered printenv output is empty: no CLAUDE_PLUGIN_ROOT, no CLAUDE_PLUGIN_DATA, no *PLUGIN*-named variable at all. Reproduced three times across plugin versions 0.0.1/0.0.3/0.0.5, and again for a Bash call issued by a plugin-defined agent (subagent_type: plugroot-probe:rootcheck-agent) rather than the main session.
Installed copy confirmed present at ~/.claude/plugins/cache/plugroot-mkt/plugroot-probe/0.0.7/ throughout, and installed_plugins.json records the matching installPath.
Finding 2 — hook receives the source directory. With the install at .../plugroot-probe/0.0.7/, the hook logged:
HOOK_CLAUDE_PLUGIN_ROOT=[/tmp/plugroot-mkt-34938/probe]
That is the marketplace source directory, which is #38699's Sc_ string-source branch symptom. The 2.1.94 changelog entry ("Fixed ${CLAUDE_PLUGIN_ROOT} resolving to the marketplace source directory instead of the installed cache for local-marketplace plugins on startup") does not cover this case at 2.1.265.
Expected Behaviour
CLAUDE_PLUGIN_ROOT resolves to the plugin's installPath in every context — hooks, MCP config, and the agent's Bash environment — for object-source and string-source plugin entries.
Impact
A plugin that ships scripts alongside its prose has no way to invoke them from the copy the harness actually loaded. The install path is version-stamped, so it cannot be written down; the variable that exists to solve that is unset where prose runs and points at the wrong tree where it is set. The practical fallback is for every callsite to read installed_plugins.json itself and extract installPath, which works but puts a registry lookup in user-facing prose.
Both findings are silent: an unset variable interpolates to the empty string, so "$CLAUDE_PLUGIN_ROOT"/scripts/x.sh becomes /scripts/x.sh and fails as a missing path rather than as a misconfiguration, while the hook case succeeds against stale files.
Environment
- Claude Code CLI 2.1.265
- macOS 15 (Darwin arm64, Apple M4 Pro)
- Local directory marketplace, plugin entry
"source": "./"