Is there an existing issue for this?
This issue exists in the latest npm version
Current Behavior
When npx <cmd> installs a directory/file: package into the npx cache under install-strategy=linked, the bin symlink is created inside the store entry at ~/.npm/_npx/<hash>/node_modules/.store/<name>@<ver>-<hash>/node_modules/.bin/<cmd> but not at the npx cache's top-level ~/.npm/_npx/<hash>/node_modules/.bin/<cmd>.
libnpmexec appends <installDir>/node_modules/.bin to the child process PATH and then spawns a shell to run the command. Because the top-level .bin/ directory is empty (or nonexistent), the shell resolves <cmd> against the system PATH instead and prints command not found, even though the reify succeeded and the bin exists deep inside .store/.
This means npx is effectively broken for any file: directory spec under install-strategy=linked.
Registry packages under linked strategy are not affected — they correctly get top-level symlinks and bin links.
Observed output
npm warn reify The "linked" install strategy is EXPERIMENTAL and may contain bugs.
sh: my-tool: command not found
Expected Behavior
After reifying into the npx cache, <installDir>/node_modules/.bin/<cmd> must exist (either as a real symlink pointing to the installed package's bin target, or as a wrapper script). The PATH-based invocation inside libnpmexec depends on this.
Steps To Reproduce
rm -rf /tmp/host-app ~/.npm/_npx
mkdir -p /tmp/host-app/dist
cat > /tmp/host-app/package.json << 'EOF'
{
"name": "host-app",
"version": "1.0.0",
"bin": { "my-tool": "dist/main.js" }
}
EOF
cat > /tmp/host-app/dist/main.js << 'EOF'
#!/usr/bin/env node
console.log('my-tool ran')
EOF
chmod +x /tmp/host-app/dist/main.js
cat > /tmp/host-app/.npmrc << 'EOF'
install-strategy=linked
EOF
cd /tmp/host-app
npm install
npx my-tool
# => npm warn reify The "linked" install strategy is EXPERIMENTAL and may contain bugs.
# => sh: my-tool: command not found
# Bin exists deep in .store/ but not at the top level
find ~/.npm/_npx -name my-tool
# => ~/.npm/_npx/<hash>/node_modules/.store/host-app@1.0.0-<hash>/node_modules/.bin/my-tool
ls ~/.npm/_npx/*/node_modules/.bin 2>&1
# => No such file or directory
Works with hoisted strategy
rm -rf /tmp/host-app/node_modules /tmp/host-app/package-lock.json /tmp/host-app/.npmrc ~/.npm/_npx
cd /tmp/host-app
npm install
npx my-tool
# => my-tool ran
Registry packages work under linked strategy
rm -rf ~/.npm/_npx
npx --install-strategy=linked cowsay@1.6.0 hello
# => works — cowsay binary is created at node_modules/.bin/cowsay
Environment
- npm: 11.12.1
- Node.js: v24.14.0
- OS Name: macOS (Darwin 25.4.0)
- npm config:
Root Cause Analysis
The isolated reifier treats the file: package as a store-only entry without creating the top-level symlink and bin link that registry packages get.
For a registry package (e.g. cowsay@1.6.0) under linked strategy, the reifier creates:
node_modules/cowsay symlink pointing to .store/cowsay@1.6.0-<hash>/node_modules/cowsay
node_modules/.bin/cowsay bin link
For a file: directory spec, the reifier only creates:
.store/host-app@1.0.0-<hash>/node_modules/host-app (the package contents)
.store/host-app@1.0.0-<hash>/node_modules/.bin/my-tool (the bin, buried in the store)
No top-level node_modules/host-app symlink or node_modules/.bin/my-tool link is created.
libnpmexec/lib/index.js then does:
binPaths.push(resolve(installDir, 'node_modules/.bin'))
This points at the (missing) top-level .bin/. The bin inside .store/ is never added to PATH, so the spawn fails.
The fix should ensure that when the isolated reifier processes a file: directory spec as a dependency (not the root project), it creates the same top-level symlink and bin link structure that registry packages get.
Surfaced while investigating #9210.
Is there an existing issue for this?
This issue exists in the latest npm version
Current Behavior
When
npx <cmd>installs a directory/file:package into the npx cache underinstall-strategy=linked, the bin symlink is created inside the store entry at~/.npm/_npx/<hash>/node_modules/.store/<name>@<ver>-<hash>/node_modules/.bin/<cmd>but not at the npx cache's top-level~/.npm/_npx/<hash>/node_modules/.bin/<cmd>.libnpmexecappends<installDir>/node_modules/.binto the child processPATHand then spawns a shell to run the command. Because the top-level.bin/directory is empty (or nonexistent), the shell resolves<cmd>against the systemPATHinstead and printscommand not found, even though the reify succeeded and the bin exists deep inside.store/.This means
npxis effectively broken for anyfile:directory spec underinstall-strategy=linked.Registry packages under linked strategy are not affected — they correctly get top-level symlinks and bin links.
Observed output
Expected Behavior
After reifying into the npx cache,
<installDir>/node_modules/.bin/<cmd>must exist (either as a real symlink pointing to the installed package's bin target, or as a wrapper script). The PATH-based invocation insidelibnpmexecdepends on this.Steps To Reproduce
Works with hoisted strategy
Registry packages work under linked strategy
Environment
install-strategy=linkedRoot Cause Analysis
The isolated reifier treats the
file:package as a store-only entry without creating the top-level symlink and bin link that registry packages get.For a registry package (e.g.
cowsay@1.6.0) under linked strategy, the reifier creates:node_modules/cowsaysymlink pointing to.store/cowsay@1.6.0-<hash>/node_modules/cowsaynode_modules/.bin/cowsaybin linkFor a
file:directory spec, the reifier only creates:.store/host-app@1.0.0-<hash>/node_modules/host-app(the package contents).store/host-app@1.0.0-<hash>/node_modules/.bin/my-tool(the bin, buried in the store)No top-level
node_modules/host-appsymlink ornode_modules/.bin/my-toollink is created.libnpmexec/lib/index.jsthen does:This points at the (missing) top-level
.bin/. The bin inside.store/is never added toPATH, so the spawn fails.The fix should ensure that when the isolated reifier processes a
file:directory spec as a dependency (not the root project), it creates the same top-level symlink and bin link structure that registry packages get.Surfaced while investigating #9210.