npx cannot discover packages installed globally via volta install
Environment
- Volta: 2.0.2
- OS: Linux (Ubuntu)
- Node: 20.20.2 (default, via Volta)
- npm: 10.9.9 (default, via Volta)
Description
Packages installed globally via volta install <package> work fine when invoked directly in the shell (the shim lives in ~/.volta/bin/ which is on PATH). However, npx <command> cannot discover these commands — npx ignores packages installed globally by Volta and instead downloads a fresh copy from the registry.
Steps to reproduce
# 1. Install a tool globally via Volta
volta install prettier
# package [email protected] / prettier / [email protected]
# 2. Invoke directly in the shell — works fine
prettier --version
# Output: 3.9.6 ✅
# 3. Enter an empty directory without prettier installed
mkdir /tmp/empty-project && cd /tmp/empty-project
# 4. Invoke via npx — not found, triggers a temporary download
npx prettier --version
# Prompt: "Need to install the following packages: [email protected]. Ok to proceed?" ❌
# 5. Verify with --no-install that npx indeed considers the package missing
npx --no-install prettier --version
# Error: "npx canceled due to missing packages and no YES option: [\"[email protected]\"]" ❌Expected behavior
npx prettier should discover and reuse the prettier installed globally by Volta (consistent with how npx reuses npm install -g prettier in a non-Volta environment), avoiding a redundant download.
Actual behavior
npx reports the package as missing and attempts a temporary download from the registry.
Root cause analysis
In npm 10.9.9, the lookup logic of npx <cmd> (without --package) lives in libnpmexec/lib/index.js and consists of 4 steps — none of which search PATH for arbitrary executables:
- Check the
binfield of the project'spackage.json(index.js:134-147) - Check whether
./node_modules/.bin/<cmd>exists locally (index.js:150-154) - Check whether
${globalBin}/<cmd>exists, whereglobalBin = $(npm prefix -g)/bin(index.js:155-157; defined innpm.js:453-456) - Check whether the package exists in the global node_modules tree (
$(npm root -g)) and its bin is present under globalBin (index.js:204-218)
If none of the 4 steps match, npx downloads the package from the registry into the npx cache.
Under Volta:
npm prefix -g
# → /home/<user>/.volta/tools/image/node/20.20.2
# i.e. globalBin = /home/<user>/.volta/tools/image/node/20.20.2/bin
# This directory only contains runtime commands such as node/npm/corepack,
# not packages installed by users via volta install
ls /home/<user>/.volta/tools/image/node/20.20.2/lib/node_modules/
# → only npm / corepack, no user-installed global packages
# Packages installed via volta install actually live in:
ls ~/.volta/tools/image/packages/prettier/bin/
# → prettier (symlink → ../lib/node_modules/prettier/bin/prettier.cjs)In other words, volta install stores global packages in ~/.volta/tools/image/packages/<pkg>/ and exposes their bins via shims in ~/.volta/bin/ (visible only to the shell via PATH). However, the locations npx relies on — $(npm prefix -g)/bin and the global node_modules tree, both of which are bound to the node image — do not contain these packages, so all 4 lookup steps miss.
For comparison: if a command happens to live in the node image's bin directory (e.g. tools installed directly via npm install -g into the node image), npx does find it via step 3 (verified: npx --no-install node --version works). This confirms the problem is not with npx's lookup mechanism itself, but that the storage location of volta install packages is invisible to npx.
Possible improvement
Expose volta install packages to $(npm prefix -g)/bin or the global node_modules tree in some way (e.g. via symlinks), so that npx's existing lookup logic can find them.
Source: volta-cli/volta