You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Deny by bare name, per install.allowScripts is now set in each install that has a dependency install script. npm reads the policy only from the installing project's own manifest.
clients/launcher has no scripted dependency and is unchanged.
verify:install-scripts guard in validate:guards (scripts/verify-install-scripts.mjs, with tests). For the root and every clients/* install, it fails on any lockfile entry flagged hasInstallScript that the install's own allowScripts doesn't decide (true/false). Matching follows npm 11.19's own script-allowed.js. The identity comes from the lockfile's resolved registry URL, not the folder name, so an alias is matched by the package it really is. The only key forms accepted are a bare name, name@*, or exact versions joined by ||, because npm ignores ^/~ ranges. A non-registry entry fails closed unless a key equals its resolved spec exactly, and a bundled dependency is always reported, since no key can decide it.
Docs: a row in docs/quality-gate.md and a diagnosis entry in the pre-push-gate skill.
One deviation from the issue
The issue proposed running npm install-scripts ls --json. That subcommand exists only from npm 11.19, and CI's build job runs Node 22's bundled npm 10.x, so in CI the guard would either fail for an unrelated reason or skip and enforce nothing. Instead the guard reads the lockfile's hasInstallScript flag. That is npm's own record for this question, and reading it keeps the guard offline and independent of npm version. On the unfixed tree it reports the same 10 entries as the issue's install-scripts ls table.
ignore-scripts is not used, so the root postinstall cascade still runs.
Caveat: CI doesn't block the scripts yet. The denials take effect wherever npm >= 11.19 installs. CI's install jobs use Node 22's npm 10.x, which ignores allowScripts, so CI still runs both scripts, as it does today. The guard enforces the review decision in CI regardless of npm version. Moving CI to npm 11 is tracked separately in #2715.
Verification
npm install-scripts ls --json (npm 11.19.1) returns [] in all six installs.
Fresh worktree npm install with the scripts skipped: no lockfile changes. On that install, npm run local:gate passed (build, coverage, smokes, Firefox smoke, Storybook).
Run against the unfixed v2/main checkout, the guard exits 1 and lists all 10 unreviewed entries.
…ones (#2712)
npm 11.19's `allowScripts` policy still runs a script that is neither
allowed nor denied, so every v2 install ran esbuild's and fsevents'
install scripts. Deny both by bare name in each install that holds them
(root: fsevents; web, cli, mcpdo, tui: esbuild + fsevents) — npm reads
the policy only from the installing project's own manifest.
Add `verify:install-scripts` to `validate:guards`: every lockfile entry
flagged `hasInstallScript` must be decided in its install's own
`allowScripts`. It reads the lockfile rather than `npm install-scripts
ls`, which CI's Node 22 npm 10.x does not have.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
…2712)
npm 11.19 honours only a bare name or exact versions joined by `||` as
policy keys, and matches a registry package by the name+version in the
lockfile's `resolved` URL, not its node_modules folder. The guard
accepted any semver range and keyed on the folder, so it could pass a
policy npm ignores. Mirror npm's matcher, fail closed for a
non-registry entry without an exact `resolved` key, and skip bundled
dependencies, whose scripts never run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
Range keys (fixed). The guard now accepts only a bare name, name@*, or exact versions joined by ||, matching npm 11.19.1's script-allowed.js. Checked live: npm ignores a ^ key, and the guard reports the script as unreviewed.
Identity from folder name (fixed). Name and version now come from the resolved registry URL, as npm's versionFromTgz derives them. Aliases are matched by the real package, non-registry entries fail closed, and bundled entries are skipped.
npm's allowScripts matcher returns "unreviewed" for a bundled
dependency unconditionally, and 11.x still runs an unreviewed script
during the bundle rebuild — so skipping them left a path no policy can
decide. Report every bundled scripted entry, with a remedy that does
not suggest an ineffective key. None exists in any install today.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
Copilot round 2: 2 findings, 1 fixed in fa4469e and 1 declined
Bundled scripts skipped (fixed). npm's matcher treats a bundled script as unreviewed and 11.x still runs it, so the guard now always reports one, with a remedy that doesn't suggest a key npm would ignore. None exists today.
Prepare scripts in non-registry dependencies bypass allowScripts review
scripts/verify-install-scripts.mjs:138
hasInstallScript is not a complete inventory of lifecycle scripts: npm's Arborist derives that lockfile flag from preinstall/install/postinstall, while npm 11.19 also gates and runs prepare for non-registry (git/file/link/remote) dependencies. A prepare-only dependency therefore has no flag here, is skipped as “reviewed,” and can execute without any allowScripts decision—the exact future-dependency path this guard is meant to stop. Either inspect those non-registry packages for prepare as well, or retain npm install-scripts output from an npm 11 preflight; add a prepare-only fixture to prevent this false negative.
…2712)
npm 11.19 also runs `prepare` for a git/file/remote dependency, and the
lockfile's `hasInstallScript` records only preinstall/install/postinstall,
so a prepare-only one passed unseen. A non-registry entry now needs an
exact `resolved` key whether or not it is flagged; link entries point at
in-repo source and stay out of scope. No install has one today.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
prepare on non-registry dependencies (fixed). npm 11.19 also runs prepare for a git/file/remote dependency, and the lockfile's hasInstallScript doesn't record it. Every non-registry entry now needs an exact resolved key, flagged or not. link entries point at in-repo source and are out of scope. No install has a non-registry entry today; a test covers the case. npm run local:gate passes on 685e6aa.
…y sources (#2712)
A bundled entry often carries no `resolved`, so a scriptless one read as
non-registry and was reported with no policy key able to clear it. Only a
`resolved` source that is not a registry tarball can run `prepare`; a
bundled entry never does, and one with no `resolved` is npm's own
registry fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
Copilot round 4: 2 findings, 1 fixed in 45c2815 and 1 declined
Scriptless bundled entries reported (fixed). This was a regression from round 3. The prepare fallback now applies only to a non-bundled entry with a non-registry resolved, and there's a regression case for it.
Update rationale for npm lifecycle script guard compatibility
scripts/verify-install-scripts.mjs:9
This temporal rationale is now outdated: npm 12.0.0 was released on July 8, 2026 and blocks unreviewed dependency lifecycle scripts by default. The guard is still needed because this repository installs with npm 10/11, so state that compatibility reason directly rather than implying default blocking is still future.
…til npm 12' (#2712)
npm 12.0.0 (2026-07-08) runs a dependency script only when allowScripts
says `true`, so blocking is already the default there; the guard is
needed because this repo installs with npm 10 and 11.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
Copilot round 5: approval recommended, 0 open findings, 1 "previously missed" note fixed in 5316f45
Outdated npm rationale (fixed). I confirmed it in npm 12.0.0's rebuild.js (published 2026-07-08): a script runs only when isScriptAllowed(...) === true. The header now says the guard is needed because this repo installs with npm 10 (CI) and 11.x, not that default blocking is still to come. This is a comment-only change.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The matcher can accept noncanonical version policies that npm leaves unreviewed, allowing scripts to run.
0 open findings
Previously missed (1)
In code that hasn't changed since last review
Document prepare fallback for non-registry sources
docs/quality-gate.md:43
This description omits the guard's deliberate prepare fallback: unreviewedScripts also reports every non-registry source with a resolved spec even when hasInstallScript is absent (scripts/verify-install-scripts.mjs:145-153). As written, a maintainer diagnosing such a failure is told the lockfile flagged a script when it did not; document that additional class here.
Copilot round 6: 0 open findings, 1 "previously missed" docs gap fixed in 105d6f6
Docs omitted the prepare fallback (fixed).docs/quality-gate.md and the pre-push-gate skill now describe the two classes reported without a flagged script: non-registry dependencies (cleared by an exact resolved key) and bundled ones (no key can clear them).
Headline on noncanonical version keys (no change). No finding came with it, and the exact-version check is npm 11.19.1's own isExactVersionDisjunction (semver.valid on each || part), so it accepts the same keys npm does. This is a docs-only change.
Copilot review loop closed. Round 7 was clean: approval recommended, 0 open findings, no inline or suppressed comments. Over 7 rounds I fixed 8 findings and declined 3 as beyond #2712 (CI's npm version, now #2715; remote tarballs with registry-shaped URLs; full npa parity for git keys).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2712
What
allowScriptsis now set in each install that has a dependency install script. npm reads the policy only from the installing project's own manifest.{ "fsevents": false }clients/{web,cli,mcpdo,tui}:{ "esbuild": false, "fsevents": false }clients/launcherhas no scripted dependency and is unchanged.verify:install-scriptsguard invalidate:guards(scripts/verify-install-scripts.mjs, with tests). For the root and everyclients/*install, it fails on any lockfile entry flaggedhasInstallScriptthat the install's ownallowScriptsdoesn't decide (true/false). Matching follows npm 11.19's ownscript-allowed.js. The identity comes from the lockfile'sresolvedregistry URL, not the folder name, so an alias is matched by the package it really is. The only key forms accepted are a bare name,name@*, or exact versions joined by||, because npm ignores^/~ranges. A non-registry entry fails closed unless a key equals itsresolvedspec exactly, and a bundled dependency is always reported, since no key can decide it.docs/quality-gate.mdand a diagnosis entry in thepre-push-gateskill.One deviation from the issue
The issue proposed running
npm install-scripts ls --json. That subcommand exists only from npm 11.19, and CI'sbuildjob runs Node 22's bundled npm 10.x, so in CI the guard would either fail for an unrelated reason or skip and enforce nothing. Instead the guard reads the lockfile'shasInstallScriptflag. That is npm's own record for this question, and reading it keeps the guard offline and independent of npm version. On the unfixed tree it reports the same 10 entries as the issue'sinstall-scripts lstable.ignore-scriptsis not used, so the rootpostinstallcascade still runs.Caveat: CI doesn't block the scripts yet. The denials take effect wherever npm >= 11.19 installs. CI's install jobs use Node 22's npm 10.x, which ignores
allowScripts, so CI still runs both scripts, as it does today. The guard enforces the review decision in CI regardless of npm version. Moving CI to npm 11 is tracked separately in #2715.Verification
npm install-scripts ls --json(npm 11.19.1) returns[]in all six installs.npm installwith the scripts skipped: no lockfile changes. On that install,npm run local:gatepassed (build, coverage, smokes, Firefox smoke, Storybook).v2/maincheckout, the guard exits 1 and lists all 10 unreviewed entries.🤖 Generated with Claude Code