Is there an existing issue for this?
This issue exists in the latest npm version
Current Behavior
Every invocation of npx <cmd> that resolves to a local directory (file: spec) triggers a full reify() cycle, even when the package is already installed in the npx cache. Registry packages correctly skip the reify on subsequent runs.
This happens regardless of install-strategy — both hoisted and linked are affected.
Expected Behavior
npx should detect that a directory spec is already installed in the npx cache and skip the reify step when the package is unchanged, just as it does for registry packages.
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
cd /tmp/host-app
npm install
npx my-tool # run 1 — reifies (expected, cold cache)
npx my-tool # run 2 — reifies again (unexpected)
npx my-tool # run 3 — reifies again (unexpected)
Contrast with a registry package:
rm -rf ~/.npm/_npx
npx cowsay@1.6.0 hello # run 1 — reifies (expected)
npx cowsay@1.6.0 hello # run 2 — skips reify (cache hit)
Confirmed with debug logging
Adding console.error('DEBUG add:', JSON.stringify(add)) before the if (add.length) check in libnpmexec/lib/index.js:
# directory spec — add is always non-empty
Run 1: DEBUG add: ["file:/private/tmp/host-app"]
Run 2: DEBUG add: ["file:/private/tmp/host-app"] ← should be []
# registry spec — add is empty on cache hit
Run 1: DEBUG add: ["cowsay@1.6.0"]
Run 2: DEBUG add: [] ← correctly cached
Environment
- npm: 11.12.1
- Node.js: v24.14.0
- OS Name: macOS (Darwin 25.4.0)
Root Cause Analysis
In libnpmexec/lib/index.js, missingFromTree() has an early return for directory specs that bypasses the cache check:
if (spec.type === 'directory') {
return { manifest }
}
Returning { manifest } (without a node) signals "not in tree — needs install." This causes the caller to unconditionally push the package onto the add list and call npxArb.reify().
For registry specs, the function instead walks the tree looking for a matching resolved:
for (const node of nodesByManifest) {
if (node.package.resolved === manifest._resolved) {
return { node } // found in cache — no install needed
}
}
Directory specs skip this check entirely, so the npx cache is never consulted.
Surfaced while investigating #9210.
Is there an existing issue for this?
This issue exists in the latest npm version
Current Behavior
Every invocation of
npx <cmd>that resolves to a local directory (file:spec) triggers a fullreify()cycle, even when the package is already installed in the npx cache. Registry packages correctly skip the reify on subsequent runs.This happens regardless of
install-strategy— bothhoistedandlinkedare affected.Expected Behavior
npxshould detect that a directory spec is already installed in the npx cache and skip the reify step when the package is unchanged, just as it does for registry packages.Steps To Reproduce
Contrast with a registry package:
Confirmed with debug logging
Adding
console.error('DEBUG add:', JSON.stringify(add))before theif (add.length)check inlibnpmexec/lib/index.js:Environment
Root Cause Analysis
In
libnpmexec/lib/index.js,missingFromTree()has an early return for directory specs that bypasses the cache check:Returning
{ manifest }(without anode) signals "not in tree — needs install." This causes the caller to unconditionally push the package onto theaddlist and callnpxArb.reify().For registry specs, the function instead walks the tree looking for a matching
resolved:Directory specs skip this check entirely, so the npx cache is never consulted.
Surfaced while investigating #9210.