[BUG] Claude Desktop NodeJS Env + NVM Path Sorting
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Summary
Claude Desktop's PATH builder enumerates ~/.nvm/versions/node/*/bin via glob and prepends the results to the spawned shell's PATH. A recent version (1.4758.0, observed via the bundled app.asar) added a .reverse() on the glob results so the lexicographically-largest directory name comes first, which usually means the newest Node wins. That partially fixes the original symptom, but the underlying mechanism is lexicographic ordering of directory names, not semantic version resolution — so it can still pick the wrong version for ordinary nvm layouts.
Related (same Desktop-app PATH-construction code path, different symptoms): [#42248](https://github.com/anthropics/claude-code/issues/42248), [#46954](https://github.com/anthropics/claude-code/issues/46954). Likely-misdiagnosed prior report of the same symptom: [#34392](https://github.com/anthropics/claude-code/issues/34392) (closed without identifying the glob enumeration).
Evidence and version comparison
Caveat on the "before/after" framing: I observed the runtime behavior of an earlier Claude Desktop build (no version captured at the time, sometime before 2026-04-27), and I extracted the source of the current build (1.4758.0). I do not have the earlier build's app.asar to diff against. The comparison below is observed-symptom-then vs. read-source-now, not a source-level diff.
Earlier build — observed runtime symptom only. Inside a Claude Code Bash with ~/.nvm/versions/node/{v16.14.2,v17.8.0,v20.11.0,v24.11.1} installed and nvm alias default v24.11.1:
$ echo "$PATH" | tr ':' '\n' | grep nvm
/Users/me/.nvm/versions/node/v16.14.2/bin
/Users/me/.nvm/versions/node/v17.8.0/bin
/Users/me/.nvm/versions/node/v20.11.0/bin
/Users/me/.nvm/versions/node/v24.11.1/bin
$ node --version
v16.14.2
Glob results were prepended in forward lexicographic order, so the lexicographically-smallest directory (v16.14.2) won.
Current build (1.4758.0) — extracted source. From /Applications/Claude.app/Contents/Resources/app.asar via grep -aoE:
function pgr() {
const e = yi.homedir();
return zr ? [
`${e}/.nvm/versions/node/*/bin`,
"/opt/homebrew/Caskroom/miniforge/base/envs/py*/bin",
"/usr/local/bin",
"/opt/homebrew/bin",
…
] : …
}
async function mgr() {
let e = [];
for (const i of pgr()) {
const r = [];
for await (const n of te.glob(i)) r.push(n);
r.reverse(); // ← changed
e = [...e, ...r];
}
const A = process.env.PATH?.split(rA.delimiter) ?? [];
return Array.from(new Set([...e, ...A]));
}
r.reverse() is the only relevant change inferable from the symptom shift. After reversing, v24.11.1 precedes v16.14.2, which is what fixed the case I originally hit. mgr() produces the final export PATH=... injected at the end of every shell snapshot under ~/.claude/shell-snapshots/snapshot-zsh-*.sh.
If anyone reading this issue has an earlier app.asar archived, please attach the corresponding mgr()/pgr() definitions so the diff can be made source-to-source rather than inferred.
What the .reverse() does and does not fix
It fixes the original alphabetical-first symptom for the common nvm layout where every directory has the same number of digits in the major component:
v16.14.2 v17.8.0 v20.11.0 v24.11.1
glob() returns these in lexicographic order; reversing puts v24.11.1 first.
It does not fix the underlying mechanism — it relies on lexicographic order matching semver order, which is only true accidentally. Real cases where it still picks wrong:
- Major-version digit count differs. Lexicographic compare is character-by-character.
v9.0.0>v10.0.0andv9.0.0>v24.0.0because9>1and9>2as characters. After.reverse(),v9.0.0wins overv24.0.0. Anyone who ever installed Node 8 or 9 alongside a current major hits this.
- Minor/patch digit counts differ within the same major.
v20.10.0vs.v20.9.0— lexicographicallyv20.9.0>v20.10.0because9>1at the first differing character. After.reverse(),v20.9.0wins.
- Pre-release / candidate suffixes.
v20.0.0-rc.1andv20.0.0sort by string compare, which has no relationship to semver precedence.
- Non-nvm directory names.
iojs-v3.xor other historical entries that nvm can leave behind sort however their leading character lands.
- Project intent ignored. Even when the glob happens to land on a version that matches the user's nvm
default, that's coincidence. A project's.nvmrcis never consulted.
Workaround
Until the resolver is fixed, uninstall Node versions you don't need so the glob can only find one:
nvm reinstall-packages <old> # if you need globals from an older version
nvm uninstall v9.x.x v16.x.x …
After restarting Claude Code the snapshot regenerates with only the remaining version on PATH.
What Should Happen?
Stop relying on directory-name order. Resolve a single Node version explicitly, then prepend only that one bin:
- Project-local
.nvmrcin the working directory (or any ancestor). ~/.nvm/alias/default(the user's nvm default).- As a last resort, parse the directory names as semver (
semver.rsort) — not lexicographic order — and pick the highest.
If preserving every installed version on PATH is intentional (so nvm use can switch within a session), at minimum sort the glob results with a semver comparator, not lexicographically, and put the resolved active version first.
Error Messages/Logs
Two concrete error blocks I captured live in your session — both directly caused by Claude Desktop selecting `v16.14.2` instead of your nvm default `v24.11.1`:
---
**1. `markdownlint-cli2` aborting in a `PostToolUse` lint hook**
This is the original symptom that started the investigation. Captured by running the hook against `README.md` while Claude Desktop's snapshot pinned PATH to v16:
markdownlint (auto-fix ran) — unfixable problems in /Users/me/dev/repo/README.md:
file:///Users/me/dev/repo/node_modules/.pnpm/string-width@8.1.0/node_modules/string-width/index.js:16
const zeroWidthClusterRegex = /^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Mark}|\p{Surrogate})+$/v;
^
SyntaxError: Invalid regular expression flags
at ESMLoader.moduleStrategy (node:internal/modules/esm/translators:115:18)
at ESMLoader.moduleProvider (node:internal/modules/esm/loader:289:14)
The regex `v` flag was added in Node 20. Under Node 16 every invocation of `markdownlint-cli2@0.22.1` crashes at module load. Because the hook returned non-zero, the agent's `Write` was hard-blocked.
---
**2. `npm install` ETARGET in a `SessionStart` hook**
Captured at session boot (the very first system reminder of the session). The session's npm was the v16.14.2-bundled npm 8.x, which failed to resolve `chokidar@^5.0.0`:
SessionStart:startup hook success: npm ERR! code ETARGET
npm ERR! notarget No matching version found for chokidar@^5.0.0.
npm ERR! notarget In most cases you or one of your dependencies are requesting
npm ERR! notarget a package version that doesn't exist.
npm ERR! A complete log of this run can be found in:
npm ERR! /Users/me/.npm/_logs/2026-04-28T00_09_32_788Z-debug-0.log
The `chokidar@5.x` line *does* exist on the public npm registry; the error was npm 8.x failing to resolve a manifest it didn't understand. After uninstalling Node 16 and switching to v24's npm 11, the same `pnpm install` succeeded without changes to `package.json`.
Steps to Reproduce
Reproduction (still demonstrable on 1.4758.0)
Install Node majors that span a digit-width boundary:
nvm install 9
nvm install 24
nvm alias default 24
In a fresh terminal: node --version → v24.x.x.
Open a new Claude Code session and run node --version in Bash → v9.x.x, because v9... > v24... lexicographically and .reverse() puts the lexicographic-greatest first.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.121
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
iTerm2
Additional Information
_No response_
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗