You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
normalizePluginJson() rewrites .claude-plugin/plugin.jsonmcpServers.command from the shipped "node" to process.execPath. On Homebrew (and any versioned Node manager) that is a version-pinned path. After the next Node upgrade the path is deleted and the MCP server stops connecting with a bare ENOENT:
ENOENT: no such file or directory, posix_spawn '/…/Cellar/node/<old>/bin/node'
The liveness guards added for the sibling cases do not apply here, and cannot: they run inside a live process at read time, but the process that would heal mcpServers.command is the MCP server itself — which is exactly what fails to spawn. There is no path back without editing the file by hand.
Why this one is not covered by the earlier fixes
The same class has been fixed four times, each for a different consumer:
mcpServers.command is the remaining one. It is also the only consumer whose failure is unrecoverable by self-heal, for the bootstrap reason above.
Where it happens
hooks/normalize-hooks.mjs:
if(srv.command==="node"&&mutated){srv.command=safeNode;// safeNode derives from process.execPath}
reached from normalizeHooksOnStartup(), which the comment marks as intentional:
// plugin.json rewrite: ALWAYS uses nodePath (MCP server must stay on Node, #543)
The intent behind #543 (keep the MCP server on Node, never Bun) is sound. The problem is only that it is implemented by pinning an absolute versioned path rather than by naming a Node that stays valid.
Reproduce
Install the plugin with a Node whose process.execPath is version-pinned (Homebrew, mise, nvm, asdf).
Confirm .claude-plugin/plugin.json now carries the absolute pinned interpreter instead of "node".
Upgrade Node so the old version directory is removed (brew upgrade node && brew cleanup).
Start a new session — the MCP server fails with ENOENT on the deleted path, and no self-heal runs.
If an absolute path is wanted, resolve to a stable one rather than the versioned snapshot — e.g. the unversioned symlink a version manager maintains (<brew-prefix>/bin/node) — so it survives upgrades.
Option 1 or 2 also fixes it retroactively for installs already carrying a pinned path, since the rewrite is re-evaluated on the next successful boot — though only for users whose server can still start.
Note on scope
Because a manual edit is currently the only recovery, this may be worth treating as higher severity than the earlier three: an affected user sees only an ENOENT naming a Node path they never configured, with no indication that a plugin wrote it.
Summary
normalizePluginJson()rewrites.claude-plugin/plugin.jsonmcpServers.commandfrom the shipped"node"toprocess.execPath. On Homebrew (and any versioned Node manager) that is a version-pinned path. After the next Node upgrade the path is deleted and the MCP server stops connecting with a bare ENOENT:The liveness guards added for the sibling cases do not apply here, and cannot: they run inside a live process at read time, but the process that would heal
mcpServers.commandis the MCP server itself — which is exactly what fails to spawn. There is no path back without editing the file by hand.Why this one is not covered by the earlier fixes
The same class has been fixed four times, each for a different consumer:
resolveJavascriptRuntime()liveness guardmcpServers.argsmcpServers.commandis the remaining one. It is also the only consumer whose failure is unrecoverable by self-heal, for the bootstrap reason above.Where it happens
hooks/normalize-hooks.mjs:reached from
normalizeHooksOnStartup(), which the comment marks as intentional:The intent behind #543 (keep the MCP server on Node, never Bun) is sound. The problem is only that it is implemented by pinning an absolute versioned path rather than by naming a Node that stays valid.
Reproduce
process.execPathis version-pinned (Homebrew, mise, nvm, asdf)..claude-plugin/plugin.jsonnow carries the absolute pinned interpreter instead of"node".brew upgrade node && brew cleanup).Possible fixes
commandas the shipped"node"and let PATH resolve it. upgrade: Native addon ABI cache missing when upgrade CLI runs under Bun runtime #543's requirement is "Node, not Bun", which barenodealready satisfies.<brew-prefix>/bin/node) — so it survives upgrades.Option 1 or 2 also fixes it retroactively for installs already carrying a pinned path, since the rewrite is re-evaluated on the next successful boot — though only for users whose server can still start.
Note on scope
Because a manual edit is currently the only recovery, this may be worth treating as higher severity than the earlier three: an affected user sees only an ENOENT naming a Node path they never configured, with no indication that a plugin wrote it.