[FEATURE] Parameterized plugin-root placeholder `${CLAUDE_PLUGIN_ROOT:<plugin-name>}` in settings.json hooks
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
${CLAUDE_PLUGIN_ROOT} only resolves inside a plugin's own hooks/hooks.json, where the owning plugin provides the context. In settings.json / settings.local.json hooks there is no such context, so the placeholder cannot be used — yet that is exactly where hooks must live when a project wants selective configuration (e.g. registering only some of a plugin's hook matchers, or different matchers per project).
Today the only workaround is hardcoding the resolved cache path:
"command": "& \"$env:USERPROFILE\\.claude\\plugins\\cache\\my-marketplace\\my-plugin\\1.1.0\\dist\\tool.exe\"",
"shell": "powershell"
This is fragile in three ways:
- Version-pinned — every plugin update moves the cache directory and silently breaks the hook.
- Marketplace-name-coupled — the path embeds the marketplace identifier, an implementation detail that can be renamed, misleading, or differ across machines.
- Shell-dependent — env-var expansion syntax differs per hook shell (
%VAR%expands nowhere,$VARonly in Git Bash,$env:VARonly in PowerShell), forcing shell pinning just to build a path.
Proposed Solution
Support a parameterized form of the existing placeholder, resolved by Claude Code itself (like ${CLAUDE_PROJECT_DIR}) before any shell sees the command:
"command": "\"${CLAUDE_PLUGIN_ROOT:my-plugin}/dist/tool.exe\""
Resolution: the installation root of the named, enabled plugin — whatever marketplace it came from and whatever version is currently installed. Ambiguity (same plugin name from two marketplaces) could be resolved with an optional qualified form ${CLAUDE_PLUGIN_ROOT:marketplace/plugin}; an unknown or disabled plugin name should fail the hook visibly rather than silently.
Benefits
- Project settings can compose plugin-shipped executables/scripts into custom hook selections without duplicating the plugin's all-or-nothing
hooks.json. - Hook registrations survive plugin updates — no re-init after every version bump.
- Paths are portable across machines and shells — no user-profile layout, marketplace naming, or shell-specific expansion baked into checked-in settings.
- Consistent with the existing placeholder model (
${CLAUDE_PROJECT_DIR},${CLAUDE_PLUGIN_ROOT},${CLAUDE_PLUGIN_DATA}) — same expansion point, just with an explicit plugin argument. A matching${CLAUDE_PLUGIN_DATA:<plugin-name>}would round the feature out.
Alternative Solutions
_No response_
Priority
High - Significant impact on productivity
Feature Category
Configuration and settings
Use Case Example
A hook plugin ships one native exe serving several independent fixes (file-format enforcement, encoding recode, search-result filtering), dispatching on the hook payload. Which features are active is controlled purely by which matchers a project registers — inherently a per-project settings.json concern. The plugin's own hooks.json can't express that selectivity, and hardcoded cache paths break on every release.
Additional Context
_No response_