cli icon indicating copy to clipboard operation
cli copied to clipboard

[BUG] npx does not preserve PATH environment variable correctly.

Open ioquatix opened this issue 2 months ago • 1 comments

Is there an existing issue for this?

  • [x] I have searched the existing issues

This issue exists in the latest npm version

  • [x] I am using the latest npm

Current Behavior

npx appears to be inserting /usr/bin ahead of all other path entries, causing problems for version managers that depend on adding specific entries before more general system entries:

> npx --no-install bash -c "env | grep PATH"
GEM_PATH=/home/samuel/.gem/ruby/3.4.6:/home/samuel/.rubies/ruby-3.4.6/lib/ruby/gems/3.4.0
PATH=/usr/bin:/home/samuel/Developer/socketry/lively/examples/game-audio/node_modules/.bin:/home/samuel/Developer/socketry/lively/examples/node_modules/.bin:/home/samuel/Developer/socketry/lively/node_modules/.bin:/home/samuel/Developer/socketry/node_modules/.bin:/home/samuel/Developer/node_modules/.bin:/home/samuel/node_modules/.bin:/home/node_modules/.bin:/node_modules/.bin:/usr/lib/node_modules/npm/node_modules/@npmcli/run-script/lib/node-gyp-bin:/home/samuel/.gem/ruby/3.4.6/bin:/home/samuel/.rubies/ruby-3.4.6/lib/ruby/gems/3.4.0/bin:/home/samuel/.rubies/ruby-3.4.6/bin:/home/samuel/Developer/jruby/jruby/bin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl

> bash -c "env | grep PATH"
GEM_PATH=/home/samuel/.gem/ruby/3.4.6:/home/samuel/.rubies/ruby-3.4.6/lib/ruby/gems/3.4.0
PATH=/home/samuel/.gem/ruby/3.4.6/bin:/home/samuel/.rubies/ruby-3.4.6/lib/ruby/gems/3.4.0/bin:/home/samuel/.rubies/ruby-3.4.6/bin:/home/samuel/Developer/jruby/jruby/bin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl

Note that /usr/bin is inserted at the start of PATH when running under npx. But it's already listed later on.

Because of that, ruby is resolved differently:

> npx --no-install bash -c "which ruby"
/usr/bin/ruby

> bash -c "which ruby"
/home/samuel/.rubies/ruby-3.4.6/bin/ruby

Expected Behavior

npx should not add system paths to the PATH environment variable.

Steps To Reproduce

See the given commands.

Environment

  • npm: 11.6.1
  • Node.js: 24.8.0
  • OS Name: Linux aiko 6.16.8-arch3-1 #1 SMP PREEMPT_DYNAMIC Mon, 22 Sep 2025 22:08:35 +0000 x86_64 GNU/Linux

ioquatix avatar Oct 06 '25 06:10 ioquatix

Analysis from AI:

Where in the code this behavior originates

  • The PATH given to the spawned child is produced by @npmcli/run-script:
    • set-path.js builds the PATH the child receives. It constructs PATH as:
      • [binPaths..., .bin for cwd + parents..., node-gyp-bin, original PATH]
  • The binPaths array gets entries pushed by higher-level logic in libnpmexec when a match is found in the global bin:
    • index.js (inside the "needPackageCommandSwap" handling) does:
      • if (globalPath && await fileExists(${globalBin}/${args[0]})) { binPaths.push(globalBin); return await run() }
    • globalBin is computed by npm.js:
      • get globalBin () { const b = this.globalPrefix; return process.platform !== 'win32' ? resolve(b, 'bin') : b }
      • On typical Linux installs globalBin is bin (because globalPrefix resolves to usr or equivalent).
  • Net result: libnpmexec pushes bin into binPaths, set-path.js places binPaths at the front of PATH, and the spawned process therefore sees bin before the user ruby shims.

Expected behavior

  • Running npx --no-install bash -c "which ruby" in a user shell that has ruby shims earlier in PATH should normally respect the user's PATH ordering and find the user-managed Ruby first (i.e. preserve PATH semantics so user shims aren't unintentionally shadowed by bin).

Root cause

  • npx/libnpmexec intentionally prefers local/global bin locations and, when it finds an executable in the global bin, pushes that global bin path into binPaths. Because set-path.js places binPaths at the front of PATH, adding globalBin causes system directories (like bin) to be prepended before the user PATH entries. If Node itself is installed in bin, that directory becomes first and hides user-managed shims that appear later in PATH.

Severity / impact

  • Moderate: causes commands invoked via npx (including interactive shells entered via npx ... sh) to see a different PATH ordering than the user expects. This can break tools that rely on per-user shims (rbenv/asdf/ruby-install/etc.), cause unexpected binaries to run, and make debugging difficult.

Suggested fixes (options)

  1. Conservative/behavioral change (recommended)

    • Stop inserting globalBin into binPaths such that it ends up at the front of PATH. If libnpmexec still wants to prefer global bins, consider appending globalBin later (after the original PATH) or add it after .bin but before the original PATH rather than prepending it. This preserves the user's PATH ordering for system/user shims.
    • File pointers:
      • push site: index.js — where binPaths.push(globalBin) is triggered.
      • PATH builder: set-path.js.
  2. Config/opt-in behavior

    • Add an option/flag to avoid adding globalBin to binPaths (e.g. --no-prepend-global-bin) so users can opt out.
  3. More targeted: when adding globalBin, ensure it is not equal to directory(dirname(process.execPath)) or otherwise guard against prepending the directory that holds the running Node executable in front of the user PATH (this addresses the common bin case).

Recommended test cases to add

  • Unit/integration tests exercising the case when:
    • user PATH contains a shim (e.g. ~/.rubies/.../bin) earlier than bin and npx is asked to run a shell or a binary that exists in bin as well.
    • verifying final PATH returned by set-path does not place bin before user shim dirs unless explicitly requested.
  • A regression test for npx --no-install bash -c 'which ruby' returning the same ruby as plain shell when user's shell PATH has shims first.

ioquatix avatar Oct 06 '25 06:10 ioquatix