Plugin MCP relative headersHelper resolved against session CWD, not plugin root
Summary
Claude Code resolves a plugin MCP server's relative headersHelper path against the session's current working directory instead of the plugin root. As a result, a plugin's auth-header helper is only found when the session is launched from the plugin's own directory; from any other cwd the helper isn't located, no auth header is produced, and the MCP server fails to connect.
Separately, ${CLAUDE_PLUGIN_ROOT} is not expanded in the headersHelper field, so plugins cannot portably reference their own helper as a workaround.
There is also a security implication: because resolution is cwd-relative, Claude Code will execute whatever ./scripts/get-headers.sh happens to exist in the directory the session was launched from.
Environment
- Claude Code 2.1.177 (cli). Also reproduced at 2.1.158 and 2.1.159, so this is persistent, not a recent regression.
- Platform: macOS (darwin), Node v24.3.0.
- A plugin declaring an HTTP MCP server with a
headersHelper, e.g.:
``json``
"mcpServers": {
"demo": {
"type": "http",
"url": "http://127.0.0.1:8765/mcp",
"headersHelper": "scripts/get-headers.sh"
}
}
Expected behavior
A relative headersHelper should resolve against the plugin root — i.e. ${CLAUDE_PLUGIN_ROOT}/<headersHelper> — independent of the session cwd, consistent with how plugin hook commands already resolve ${CLAUDE_PLUGIN_ROOT}. Additionally, ${CLAUDE_PLUGIN_ROOT} should be expanded if present in the headersHelper value.
Actual behavior
- A relative
headersHelperis resolved against the session cwd. Launched from a directory without that relative path, the helper spawn fails (ENOENT). - The MCP debug log shows
Executing headersHelper to get dynamic headersimmediately followed byError getting headers from headersHelper: headersHelper for MCP server '<name>' did not return a valid value, a failure in ~12 ms, then a fall-through to OAuth discovery and a failed connection. ${CLAUDE_PLUGIN_ROOT}written literally intoheadersHelperis not expanded.
Reproduction
- Install a plugin whose
.claude-plugin/plugin.jsondeclares an HTTP MCP server with a relativeheadersHelper: "scripts/get-headers.sh", wherescripts/get-headers.sh(in the plugin root) prints{"Authorization": "Bearer ..."}. - Start Claude Code with cwd = any directory other than the plugin root (e.g.
/tmp). - The MCP server fails to connect; the per-server MCP debug log shows
headersHelper ... did not return a valid value. - Start Claude Code with cwd = the plugin root (which contains
./scripts/get-headers.sh) → it connects. This cwd-dependence is the tell.
Proof it is cwd-relative (not an env / $HOME issue)
Placing a probe helper at <cwd>/scripts/get-headers.sh that logs its invocation and delegates to the real helper: Claude Code executed that cwd-local file and logged cwd=/tmp — confirming it resolved scripts/get-headers.sh against the session cwd. (An earlier $HOME-unset hypothesis was ruled out — the captured invocation showed HOME was present.)
Impact
- Any plugin that authenticates an HTTP/SSE MCP server via a relative
headersHelperworks only when launched from the plugin root — broken for normal use from other working directories. - An absolute-path workaround written into the installed manifest is reverted by
/plugin update(which reinstalls the committed relative path), so it must be re-applied after every update.
Security implication
Cwd-relative resolution means Claude Code will execute an attacker-supplied ./scripts/get-headers.sh if a session is launched inside a directory that contains one (e.g. a cloned repo). The helper runs as the user at connect time. Resolution should be anchored to the trusted plugin root, never the (potentially untrusted) cwd.
Suggested fix
- Resolve a relative
headersHelperagainst the plugin root (${CLAUDE_PLUGIN_ROOT}), consistent with hook command resolution — not the session cwd. - Expand
${CLAUDE_PLUGIN_ROOT}in theheadersHelpervalue. - (Security) Never resolve plugin-declared executables against the session cwd.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗