Skip to content

[BUG] npx --yes <pkg> fails with 'sh: <bin>: command not found' — npx cache bin dir is not added to the spawned shell's PATH #9870

Description

@JimLiuxinghai

Is there an existing issue for this?

  • I have searched the existing issues

Summary

npx --yes <pkg> <args> (and npm exec -- <pkg> <args>) downloads the package into ~/.npm/_npx/<hash>/node_modules/ and creates a bin symlink at ~/.npm/_npx/<hash>/node_modules/.bin/<bin-name>, but then runs the command via sh -c "<bin-name> <args>" without putting the npx cache bin directory in the spawned shell's PATH. The shell therefore cannot find the bin by name and reports sh: <bin-name>: command not found.

Running the same package via node <cache>/node_modules/<pkg>/<bin-path> works perfectly, and the issue reproduces regardless of Node major version (verified on Node 20 and Node 22) and npm major version (verified on npm 10.9.4 and npm 11.x).

Current Behavior

$ npx --yes @deepseek-ai/dsh web
sh: dsh: command not found

The package is downloaded (visible at ~/.npm/_npx/<hash>/node_modules/@deepseek-ai/dsh/) and the bin symlink is created:

$ ls -l ~/.npm/_npx/<hash>/node_modules/.bin/dsh
lrwxr-xr-x ... dsh -> ../@deepseek-ai/dsh/lib/bin.js

But the sh -c "dsh web" invocation that npm uses cannot resolve dsh because the npx cache's bin directory is not on PATH. After failing, npm exits 0 with no other diagnostic, leaving users with no actionable feedback.

Expected Behavior

npx --yes @deepseek-ai/dsh web should succeed, just like npm i -g @deepseek-ai/dsh && dsh web does, because both place the bin in the user's PATH.

Steps To Reproduce

mkdir /tmp/repro && cd /tmp/repro
npx --yes @deepseek-ai/dsh web
# → sh: dsh: command not found

Any package whose package.json declares a bin (and the bin file is a Node script) reproduces this on the current npm, e.g.:

mkdir /tmp/repro2 && cd /tmp/repro2
npx --yes http-server
# → sh: http-server: command not found

(I verified this against http-server@14.1.1 on npm 11.x in addition to @deepseek-ai/dsh@0.1.0-rc.6.)

Environment

  • npm: 11.6.2 (verified; also reproduces on 11.1.0 and 10.9.4)
  • Node.js: v22.20.0 (verified; also reproduces on v20.18.3)
  • OS: macOS 14 (Darwin 24.6.0)

Root Cause

libnpmexec@npmcli/run-script (run-script-pkg.js) → @npmcli/promise-spawn. The shell command is built by make-spawn-args.js, and the PATH environment variable for the child shell is assembled by set-path.js:

// node_modules/@npmcli/run-script/lib/set-path.js (lines 22–28)
let p = projectPath      // projectPath === process.cwd()
let pp
do {
  pathArr.push(resolve(p, 'node_modules', '.bin'))
  pp = p
  p = dirname(p)
} while (p !== pp)

This loop walks up from process.cwd() looking for node_modules/.bin at every ancestor directory, then appends the inherited PATH last. It never includes ~/.npm/_npx/<hash>/node_modules/.bin — the actual location of the bin symlink that libnpmexec just created. The binPaths parameter that libnpmexec could pass is also not honored for this purpose.

Meanwhile, promise-spawn ultimately invokes sh -c "<bin-name> <args>" (see node_modules/@npmcli/promise-spawn/lib/index.js:73,121), so a missing PATH entry means sh cannot find the bin by name and exits with command not found. The exit status is non-zero, but libnpmexec/run-script.js does not surface it to the user.

This is independent of Node version and npm version (verified by running node /tmp/npm10/package/bin/npx-cli.js from a downloaded npm 10.9.4 tarball on Node 22.20, which reproduces the exact same error).

Suggested Fix

In @npmcli/run-script/lib/run-script-pkg.js (or libnpmexec), when binPaths is provided by libnpmexec and includes the npx cache bin directory, prepend them to pathArr in set-path.js so the spawned shell can resolve bin names. A minimal patch would be to honor the existing binPaths argument in set-path.js (currently it only uses binPaths when the caller explicitly passes binPaths: true, conflating "should I use binPaths?" with "are binPaths provided?"):

// set-path.js — treat binPaths as a list of dirs to prepend, not a boolean
if (binPaths && Array.isArray(binPaths)) {
  pathArr.push(...binPaths)
}

…or, in libnpmexec, pass the npx cache bin dir explicitly when invoking run-script.js.

Workarounds

Until this is fixed, users can:

  1. npm i -g <pkg> and invoke the bin directly (always works).
  2. Invoke the bin via the cache path: node $(npm config get cache)/_npx/<hash>/node_modules/<pkg>/<bin-path> (works once the cache is populated).
  3. Prepend the cache bin to PATH for the npx invocation (works):
    export PATH="$(npm config get cache)/_npx/*/node_modules/.bin:$PATH"
    npx --yes @deepseek-ai/dsh web

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions