#9951·npm

[BUG] `npm link` warns about install scripts that no allowScripts mechanism can cover

Author: konardCreated Sep 4, 2026Updated Sep 16, 2026

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:

bash
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:

  1. npm link resolves an allowScripts policy like every other reify path, so allowScripts / --allow-scripts / .npmrc can approve the linked package and npm approve-scripts can see it; or
  2. 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-scripts refuses to list).

Whichever is chosen, the warning and the remediation should agree with each other.

Steps To Reproduce

bash
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.jslinkInstall() (the npm link <pkg> form) resolves the policy and threads it into both the Arborist constructor and reify():

javascript
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:

javascript
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:

  1. In linkPkg(), mirror linkInstall():

    javascript
    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.global is false during npm link (it throws ELINKGLOBAL if set), so resolveAllowScripts reads the project package.json — which is the right layer here, since the package being linked is the project.

  2. In collectUnreviewedScripts, also skip a node that is the target of a link added from the local prefix — or, equivalently, have link mark it. It is the user's own source tree, not a third-party dependency, and it is already exempt in the sibling checks: isProjectRoot is 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