Skip to content

fix(hooks): persist a stable interpreter path, not a version-pinned snapshot (#1090) - #1183

Open
brujack wants to merge 1 commit into
mksglu:mainfrom
brujack:fix/stable-node-path-1090
Open

brujack wants to merge 1 commit into
mksglu:mainfrom
brujack:fix/stable-node-path-1090

Conversation

@brujack

@brujack brujack commented Sep 19, 2026

Copy link
Copy Markdown

Fixes #1090.

Problem

normalizeHooksJsonOnly and normalizeHooksOnStartup write process.execPath into hooks/hooks.json and .claude-plugin/plugin.json (mcpServers.command). Under Homebrew/linuxbrew, and other version managers, that is a versioned path such as /home/linuxbrew/.linuxbrew/Cellar/node/26.8.2/bin/node, which the next brew upgrade node removes. Every hook then fails with /bin/sh: 1: <path>: not found, and the MCP server cannot start, so it can never run the normalization that would repair it.

Change

Adds resolveStableInterpreterPath(execPath) to hooks/normalize-hooks.mjs and uses it in both places an interpreter path is written. If execPath sits under a version-numbered directory, it looks on PATH for a same-named binary whose realpath matches execPath's and whose own path is not version-numbered, such as Homebrew's <prefix>/bin/node, and writes that instead. If there is no such candidate (plain nvm/asdf), execPath is written unchanged, so the nvm/mise behaviour from #582 is kept.

Scope

This prevents new version-pinned writes. It does not rewrite a cache that already holds one, because needsHookNormalization() only fires on the placeholder or a stale cache-version segment. Existing installs therefore pick up the fix when a plugin update extracts a new version. If you'd rather also convert an existing live versioned pin at MCP start, a trigger in needsHookNormalization() would do it; I kept it out to keep this change small.

Testing

  • 7 new tests in tests/hooks/windows-hooks-normalization.test.ts. Five cover resolveStableInterpreterPath: stable sibling, not version-pinned, no stable sibling, a same-named binary with a different realpath, and a version-pinned PATH entry ahead of the stable one. Two go end to end through normalizeHooksOnStartup with a real tmpdir and symlink fixture.
  • The new tests failed against main (7 failed of 23) and pass with the change (23 of 23).
  • Full npx vitest run: 4715 passed, 34 skipped. npm run typecheck and npm run build are clean.
  • Not tested in a live Claude Code session against this branch.

Note: CONTRIBUTING.md says to branch from next, but origin/next was last updated 2026-08-12 and lacks recent changes to normalize-hooks.mjs, so this is based on main.

🤖 Generated with Claude Code

https://claude.ai/code/session_014XqABrMmnsqj4xkck4VesT

…napshot (mksglu#1090)

normalizeHooksJsonOnly/normalizeHooksOnStartup bake process.execPath verbatim
into hooks.json commands and plugin.json's mcpServers.command. On a version
manager (Homebrew Cellar, nvm, asdf, mise) that path is a versioned snapshot
that the next upgrade deletes. For mcpServers.command specifically there is
no self-heal once that happens: the process that would re-run this
normalization is the MCP server itself, which is exactly what fails to spawn.

Add resolveStableInterpreterPath(): before persisting, prefer a same-named
PATH entry whose realpath matches execPath's realpath and whose own path is
not itself version-pinned (e.g. Homebrew's unversioned <prefix>/bin/node
symlink). Falls back to execPath unchanged when no such candidate exists,
preserving PR mksglu#582's nvm/mise fix (bare "node" is not reliably on PATH for
those managers).

Wired into both normalizeHooksJsonOnly (hooks.json) and the plugin.json
branch of normalizeHooksOnStartup, so the same fix also closes the
write-time version of the hooks.json variant of this defect, not only the
plugin.json case mksglu#1090 reports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XqABrMmnsqj4xkck4VesT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin.json mcpServers.command pinned to process.execPath has no liveness guard — and the MCP server cannot self-heal it

1 participant