/doctor false-positive: warns 'Missing environment variables: CLAUDE_PLUGIN_ROOT' for plugin MCP servers

Resolved 💬 1 comment Opened Apr 17, 2026 by mollydoo Closed May 25, 2026

Summary

/doctor warns that plugin .mcp.json entries are missing the CLAUDE_PLUGIN_ROOT environment variable, even though the MCP servers are healthy and working. The warning is a lint-time false positive: /doctor checks the current shell env for every ${VAR} referenced in .mcp.json, but CLAUDE_PLUGIN_ROOT is injected by the plugin launcher at subprocess-spawn time — it's never present in the shell env that /doctor inspects.

Steps to reproduce

  1. Install any plugin whose bundled .mcp.json uses ${CLAUDE_PLUGIN_ROOT} to point at a server entry-point. Minimal example:

``json
{
"mcpServers": {
"example": {
"command": "uv",
"args": [
"run",
"--project",
"${CLAUDE_PLUGIN_ROOT}",
"fastmcp",
"run",
"${CLAUDE_PLUGIN_ROOT}/server.py"
]
}
}
}
``

  1. Enable the plugin. The MCP server starts fine — ${CLAUDE_PLUGIN_ROOT} resolves to the cached plugin path and server.py loads normally.
  2. Run /doctor.
  3. Observe:

``
Warning mcpServers.example: Missing environment variables: CLAUDE_PLUGIN_ROOT
``

Expected behaviour

/doctor should recognise CLAUDE_PLUGIN_ROOT (and CLAUDE_PLUGIN_DATA) as plugin-system-managed variables that are guaranteed to be injected at MCP launch, and not flag them as missing. The warning should only surface for variables that are genuinely the user's responsibility to set.

Actual behaviour

The warning fires on every /doctor run and can't be silenced without hardcoding the plugin cache path in .mcp.json (which breaks portability and defeats the purpose of the placeholder).

Workarounds considered

  • Hardcode the absolute cache path — kills the warning but non-portable, breaks on plugin updates (cache path includes the version).
  • Export CLAUDE_PLUGIN_ROOT=/anything in the parent shell — would satisfy the lint, but plugin-launcher env-inheritance semantics aren't documented. If the launcher merges parent env with injected values instead of strictly overriding, the dummy could leak into the MCP subprocess and break path resolution.
  • Ignore the warning — what users end up doing.

Docs reference

The plugin reference docs state that CLAUDE_PLUGIN_ROOT and CLAUDE_PLUGIN_DATA are substituted inline in MCP / LSP server configs, hook commands, skill content, and agent content. The substitution works correctly at runtime; only /doctor's lint step is unaware of it.

Related

  • #41003 — /doctor validates individual MCP config files instead of merged result (similar class: context-blind linting).
  • #49983 — flags intentional cross-platform MCP config as warnings (alarm fatigue from /doctor).
  • #47789 — headersHelper does not expand ${CLAUDE_PLUGIN_ROOT} (different: that one is a genuine runtime bug, not a lint false positive).

Environment

  • Claude Code: v2.1.112
  • Platform: macOS 25.4.0 (Darwin, zsh)
  • Plugin installed via local marketplace (claude plugin install <name>@<marketplace> --scope user)

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗