Skip to content

chore(deps): deny esbuild/fsevents install scripts; guard unreviewed ones - #2714

Merged
cliffhall merged 7 commits into
v2/mainfrom
v2/chore/2712-deny-install-scripts
Oct 13, 2026
Merged

cliffhall merged 7 commits into
v2/mainfrom
v2/chore/2712-deny-install-scripts

Conversation

@cliffhall

@cliffhall cliffhall commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Closes #2712

What

  1. 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.
    • root: { "fsevents": false }
    • clients/{web,cli,mcpdo,tui}: { "esbuild": false, "fsevents": false }
    • clients/launcher has no scripted dependency and is unchanged.
  2. 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.
  3. 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.

🤖 Generated with Claude Code

…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The guard mismatches npm’s identity and version-policy semantics, while CI’s npm 10 installs ignore the denials entirely.

4 open findings
What changed in this PR

Adds explicit dependency install-script denials and a validation guard across Inspector’s independent installs.

Changes:

  • Denies esbuild and/or fsevents scripts per install.
  • Adds and tests verify:install-scripts.
  • Documents the guard and remediation workflow.
File Description
package.json Adds root denial and guard command.
clients/​web/​package.json Adds web install-script denials.
clients/​cli/​package.json Adds CLI install-script denials.
clients/​mcpdo/​package.json Adds mcpdo install-script denials.
clients/​tui/​package.json Adds TUI install-script denials.
scripts/​verify-install-scripts.mjs Implements lockfile policy validation.
scripts/​verify-install-scripts.test.mjs Tests policy matching and install discovery.
docs/​quality-gate.md Documents the new guard.
.claude/​skills/​pre-push-gate/​SKILL.md Adds failure diagnosis guidance.

🧠 Review effort: Balanced


💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread package.json
Comment thread scripts/verify-install-scripts.mjs Outdated
Comment thread scripts/verify-install-scripts.mjs Outdated
Comment thread scripts/verify-install-scripts.test.mjs Outdated
…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>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 1: 4 findings, 3 fixed in bfa1fbf and 1 declined (filed separately)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The guard can misclassify remote tarballs and overlooks runnable bundled dependency scripts.

2 open findings
4 resolved since last review

🧠 Review effort: Balanced

Comment thread scripts/verify-install-scripts.mjs
Comment thread scripts/verify-install-scripts.mjs Outdated
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>
@cliffhall

Copy link
Copy Markdown
Member Author

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.
  • Remote tarball with a registry-shaped URL (declined). The guard uses the same URL-shape test as npm's own lockfile fallback. Matching npm exactly would mean rebuilding Arborist's edge resolution, which goes beyond Deny unneeded esbuild/fsevents install scripts in every v2 install, and guard against unreviewed ones #2712. Reasoning is in the thread.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The guard misses prepare-only scripts from non-registry dependencies, allowing them to execute without review.

0 open findings

2 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity 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.

🧠 Review effort: Balanced

…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>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 3: 0 inline findings, 1 "previously missed" finding, fixed in 685e6aa

  • 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The guard misclassifies scriptless bundled dependencies and rejects valid npm-generated non-registry policies.

2 open findings

🧠 Review effort: Balanced

Comment thread scripts/verify-install-scripts.mjs
Comment thread scripts/verify-install-scripts.mjs
…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>
@cliffhall

Copy link
Copy Markdown
Member Author

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.
  • Git/file key forms npm also accepts (declined). The guard is stricter than npm but never more permissive, and the exact-resolved key it asks for is honored by npm. Full npa/matchGit parity is beyond Deny unneeded esbuild/fsevents install scripts in every v2 install, and guard against unreviewed ones #2712, and no install has a non-registry dependency.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The implementation satisfies the issue with focused tests; only a non-blocking outdated npm-version rationale remains.

0 open findings

2 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Low severity 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.

🧠 Review effort: Balanced

…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>
@cliffhall

Copy link
Copy Markdown
Member Author

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.

@cliffhall
cliffhall requested a balanced review from Copilot October 13, 2026 05:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Low severity 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.

🧠 Review effort: Balanced

…pts reports (#2712)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

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 AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The policies, guard behavior, tests, and documentation consistently satisfy issue #2712, with the npm 10 CI limitation explicitly tracked separately.

0 open findings

🧠 Review effort: Balanced

@cliffhall

Copy link
Copy Markdown
Member Author

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deny unneeded esbuild/fsevents install scripts in every v2 install, and guard against unreviewed ones

2 participants