[BUG] npx does not preserve PATH environment variable correctly.
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
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]
- set-path.js builds the PATH the child receives. It constructs PATH as:
- The
binPathsarray 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() }
globalBinis computed by npm.js:get globalBin () { const b = this.globalPrefix; return process.platform !== 'win32' ? resolve(b, 'bin') : b }- On typical Linux installs
globalBinis bin (becauseglobalPrefixresolves to usr or equivalent).
- index.js (inside the "needPackageCommandSwap" handling) does:
- Net result: libnpmexec pushes bin into
binPaths, set-path.js placesbinPathsat 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 placesbinPathsat the front of PATH, addingglobalBincauses 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)
-
Conservative/behavioral change (recommended)
- Stop inserting
globalBinintobinPathssuch that it ends up at the front of PATH. If libnpmexec still wants to prefer global bins, consider appendingglobalBinlater (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.
- push site: index.js — where
- Stop inserting
-
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.
- Add an option/flag to avoid adding globalBin to binPaths (e.g.
-
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.
- user PATH contains a shim (e.g.
- 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.