Plugin subagents can't resolve ${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PROJECT_DIR} — no way to read plugin-bundled files from a subagent
What happened
A subagent dispatched from a plugin cannot resolve ${CLAUDE_PLUGIN_ROOT} or ${CLAUDE_PROJECT_DIR} — the tokens reach the subagent's tool calls as literal strings. Since a subagent runs with the project as its cwd and has no handle on the plugin's install directory, there is no way for a plugin's subagent to read files bundled inside that plugin (shared standards docs, per-action contracts, prompt partials, etc.).
The surprising part is the asymmetry — the same variables resolve everywhere except subagents (all verified on Claude Code 2.1.166 via headless --output-format stream-json runs):
- SessionStart hooks resolve them —
cat "${CLAUDE_PLUGIN_ROOT}/kernel/BOOTSTRAP.md"works. - Slash commands resolve
${CLAUDE_PLUGIN_ROOT}for their own file reads — a command reads${CLAUDE_PLUGIN_ROOT}/docs/CATALOG.mdfine. - Subagents do NOT — handing a subagent's
Readthe path${CLAUDE_PLUGIN_ROOT}/kernel/KERNEL.mdfails withFile does not exist. Note: your current working directory is <project>. - A command's Bash env also lacks it —
echo "$CLAUDE_PLUGIN_ROOT"prints empty, so an orchestrator command can't even resolve-then-pass the path to the agents it spawns without brittle filesystem hunting (which in our case surfaced a stale orphaned plugin cache under~/.claude/plugins/cache/.../0.1.0/).
Repro
- A plugin with a bundled file
kernel/STANDARDS.mdand one subagentmyplugin:worker. - In a session with the plugin loaded, dispatch
myplugin:workerand instruct it toRead ${CLAUDE_PLUGIN_ROOT}/kernel/STANDARDS.md. - The
Readreceives the literal${CLAUDE_PLUGIN_ROOT}/...and fails. - Contrast: the identical path resolves in a SessionStart hook and in a slash command.
Why it matters
There's no clean way to give a plugin's agents a shared contract / base definition / "inheritance." Every workaround has a real cost:
- Paste into the spawn prompt (orchestrator reads the shared doc and injects its text into every agent prompt) — runtime copy rather than reference, bloats every spawn, couples delivery to one command.
- Duplicate inline in each agent definition — defeats DRY, drifts.
- Stage into the project filesystem (a SessionStart hook copies the plugin file into
<project>/.actionflows/so subagents can read it by relative path) — works, but the plugin then writes identical, non-project-specific content into every project's tree purely as a delivery vehicle.
Requests (in priority order)
- Expose
${CLAUDE_PLUGIN_ROOT}(and${CLAUDE_PROJECT_DIR}) to subagents — substitute them in subagent prompt text and/or make them resolvable by the subagent's tools, the same way hooks and commands already get them. - Native agent inheritance / includes — let a subagent definition reference shared partials bundled in the plugin (e.g. an
extends:/include:that composes the system prompt from a base file at load time). This is the capability actually needed. - At minimum, a documented, stable way for a command to obtain the resolved absolute
${CLAUDE_PLUGIN_ROOT}, so an orchestrator can pass it to the subagents it spawns.
Environment
- Claude Code 2.1.166
- Context: building a multi-agent orchestration plugin whose subagents need a shared, DRY "essential standards" contract.
Happy to share the stream-json transcripts demonstrating each of the four resolution behaviors above.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗