Is there an existing issue for this?
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:
npm i -g <pkg> and invoke the bin directly (always works).
- 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).
- 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
Is there an existing issue for this?
Summary
npx --yes <pkg> <args>(andnpm 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 viash -c "<bin-name> <args>"without putting the npx cache bin directory in the spawned shell'sPATH. The shell therefore cannot find the bin by name and reportssh: <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 foundThe package is downloaded (visible at
~/.npm/_npx/<hash>/node_modules/@deepseek-ai/dsh/) and the bin symlink is created:But the
sh -c "dsh web"invocation that npm uses cannot resolvedshbecause the npx cache's bin directory is not onPATH. After failing, npm exits 0 with no other diagnostic, leaving users with no actionable feedback.Expected Behavior
npx --yes @deepseek-ai/dsh webshould succeed, just likenpm i -g @deepseek-ai/dsh && dsh webdoes, because both place the bin in the user'sPATH.Steps To Reproduce
Any package whose
package.jsondeclares a bin (and the bin file is a Node script) reproduces this on the current npm, e.g.:(I verified this against
http-server@14.1.1on npm 11.x in addition to@deepseek-ai/dsh@0.1.0-rc.6.)Environment
Root Cause
libnpmexec→@npmcli/run-script(run-script-pkg.js) →@npmcli/promise-spawn. The shell command is built bymake-spawn-args.js, and thePATHenvironment variable for the child shell is assembled byset-path.js:This loop walks up from
process.cwd()looking fornode_modules/.binat every ancestor directory, then appends the inheritedPATHlast. It never includes~/.npm/_npx/<hash>/node_modules/.bin— the actual location of the bin symlink thatlibnpmexecjust created. ThebinPathsparameter thatlibnpmexeccould pass is also not honored for this purpose.Meanwhile,
promise-spawnultimately invokessh -c "<bin-name> <args>"(seenode_modules/@npmcli/promise-spawn/lib/index.js:73,121), so a missingPATHentry meansshcannot find the bin by name and exits withcommand not found. The exit status is non-zero, butlibnpmexec/run-script.jsdoes 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.jsfrom 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(orlibnpmexec), whenbinPathsis provided bylibnpmexecand includes the npx cache bin directory, prepend them topathArrinset-path.jsso the spawned shell can resolve bin names. A minimal patch would be to honor the existingbinPathsargument inset-path.js(currently it only usesbinPathswhen the caller explicitly passesbinPaths: true, conflating "should I use binPaths?" with "are binPaths provided?"):…or, in
libnpmexec, pass the npx cache bin dir explicitly when invokingrun-script.js.Workarounds
Until this is fixed, users can:
npm i -g <pkg>and invoke the bin directly (always works).node $(npm config get cache)/_npx/<hash>/node_modules/<pkg>/<bin-path>(works once the cache is populated).PATHfor the npx invocation (works):