[BUG] `npm link` warns about install scripts that no allowScripts mechanism can cover
Is there an existing issue for this?
I searched open and closed issues for link + allowScripts. The closest is #9681 (denying scripts drops .bin links), which is a different code path.
This issue exists in the latest npm version
Reproduced on npm 11.17.0.
Current Behavior
npm link (no arguments — the "publish this folder to the global prefix" form) warns that the linked package's install scripts are not covered by allowScripts:
npm warn allow-scripts 1 package has install scripts not yet covered by allowScripts:
npm warn allow-scripts @fixture/[email protected] (prepare: node -e "...")
npm warn allow-scripts
npm warn allow-scripts Run `npm approve-scripts --allow-scripts-pending` to review, or `npm approve-scripts <pkg>` to allow.The warning is unactionable. The script does run, but no allowScripts mechanism can cover it, and the remediation the warning prints does not work:
| attempt | warning | prepare ran |
|---|---|---|
| baseline | yes | yes |
allowScripts in package.json, bare name |
yes | yes |
allowScripts in package.json, name@version |
yes | yes |
allow-scripts=<name> in .npmrc |
yes | yes |
npm link --allow-scripts=<name> |
yes | yes |
npm link --ignore-scripts |
no | no |
$ npm approve-scripts --allow-scripts-pending
No packages with unreviewed install scripts.So a project that has adopted allowScripts and wants a clean, warning-free CI log has exactly one lever, --ignore-scripts, which also skips the script rather than approving it.
Expected Behavior
Either of:
npm linkresolves anallowScriptspolicy like every other reify path, soallowScripts/--allow-scripts/.npmrccan approve the linked package andnpm approve-scriptscan see it; or- the linked package is treated as not-reviewable and excluded from the advisory warning entirely (it is the user's own project, the same package
npm approve-scriptsrefuses to list).
Whichever is chosen, the warning and the remediation should agree with each other.
Steps To Reproduce
work=$(mktemp -d); mkdir -p "$work/pkg" "$work/global"
cat > "$work/pkg/package.json" <<'JSON'
{
"name": "@fixture/link-me",
"version": "1.0.0",
"bin": { "fixture": "./bin.mjs" },
"scripts": { "prepare": "node -e \"console.error('PREPARE RAN')\"" },
"allowScripts": { "@fixture/link-me": true }
}
JSON
printf '#!/usr/bin/env node\nconsole.log("ok");\n' > "$work/pkg/bin.mjs"
cd "$work/pkg" && npm_config_prefix="$work/global" npm link
# -> warns, despite the allowScripts entry above
npm_config_prefix="$work/global" npm approve-scripts --allow-scripts-pending
# -> "No packages with unreviewed install scripts."A script that walks the whole matrix in the table above is here: https://github.com/link-assistant/hive-mind/blob/main/experiments/npm-link-allow-scripts.sh
Root cause
Two independent causes, both in the no-argument path.
1. linkPkg() never resolves a policy. lib/commands/link.js — linkInstall() (the npm link <pkg> form) resolves the policy and threads it into both the Arborist constructor and reify():
const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm)
const localArb = new Arborist({ ..., allowScripts: allowScriptsPolicy })
await localArb.reify({ ..., allowScripts: allowScriptsPolicy })linkPkg() (the no-argument form) does not, so Arborist is constructed with no policy and every candidate is "unreviewed" no matter what the user configures:
async linkPkg () {
const wsp = this.workspacePaths
const paths = wsp && wsp.length ? wsp : [this.npm.prefix]
const add = paths.map(path => `file:${path}`)
const globalTop = resolve(this.npm.globalDir, '..')
const Arborist = require('@npmcli/arborist')
const arb = new Arborist({
...this.npm.flatOptions,
Arborist,
path: globalTop,
global: true,
})
await arb.reify({ add })
await reifyFinish(this.npm, arb)
}2. Even with a policy, the entry could not match. lib/script-allowed.js matches a file: node exactly on node.resolved, which for this node is a relative path from the global prefix (e.g. file:../../../../pkg) — it varies with the prefix and the source directory, so there is no stable string a user could put in allowScripts. This is why --allow-scripts=@fixture/link-me also fails: the name never enters the comparison.
Relatedly, collectUnreviewedScripts in @npmcli/arborist/lib/unreviewed-scripts.js skips isProjectRoot, isWorkspace, isLink and inBundle. In the global tree the Link node is skipped, but its target — the user's own project directory — is none of those four, so it is reported. That target is exactly the package npm approve-scripts declines to list, which is the source of the contradiction.
Suggested fix
Minimal, keeping the two halves separable:
In
linkPkg(), mirrorlinkInstall():const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm) const arb = new Arborist({ ...this.npm.flatOptions, Arborist, path: globalTop, global: true, allowScripts: allowScriptsPolicy }) await arb.reify({ add, allowScripts: allowScriptsPolicy })Note
this.npm.globalis false duringnpm link(it throwsELINKGLOBALif set), soresolveAllowScriptsreads the projectpackage.json— which is the right layer here, since the package being linked is the project.In
collectUnreviewedScripts, also skip a node that is the target of a link added from the local prefix — or, equivalently, havelinkmark it. It is the user's own source tree, not a third-party dependency, and it is already exempt in the sibling checks:isProjectRootis skipped for exactly this reason, and it only stops applying because the tree being reified is the global one.
Fix 2 alone removes the contradiction. Fix 1 alone makes the warning reviewable but only if the resolved matching in cause 2 is also addressed. If only one lands, fix 2 is the one that makes the current output self-consistent.
Workaround
npm link --ignore-scripts, accepting that the package's own prepare no longer runs during the link.
Environment
- npm: 11.17.0
- Node.js: v20.20.2 (also seen on 24.x)
- OS: Ubuntu (Linux 6.8)
- platform: linux
Source: npm/cli