npm / npm/cli

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

Open
#9,951 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
10.1k
Forks
4.7k
Avg merge
2d 2h
Merged PRs (30d)
19

Description

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/link-me@1.0.0 (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
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():

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:

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

    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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the no-argument linkPkg() and linkInstall() paths in lib/commands/link.js, then read lib/script-allowed.js and @npmcli/arborist/lib/unreviewed-scripts.js. Run the reproduction matrix to compare policy resolution, link targets, and warning behavior. Done means npm link either honors the configured policy or omits the linked project from the advisory warning, with npm approve-scripts reporting consistently.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
cli, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.