#9992·npm

[BUG] `npm install-scripts approve`/`deny` ignore `--dry-run` and write to package.json

Author: abkrimCreated Sep 16, 2026Updated Sep 16, 2026
  • I have searched the existing issues
  • I am using the latest npm
  • This is not solely a request to bump a dependency for a CVE

Current Behavior

npm install-scripts approve <pkg> --dry-run and npm install-scripts deny <pkg> --dry-run mutate package.json instead of previewing. dry-run is declared for this command (lib/commands/install-scripts.js: static params = ['all', 'allow-scripts-pin', 'dry-run', 'json']), so a command whose stated purpose is to preview is applying the write.

The sibling subcommand prune gets this right, which suggests the write path was simply not updated: it reads the flag and reports in the conditional.

$ npm install-scripts prune --dry-run
Would remove 1 unused allowScripts entry:
  [email protected] (package not installed)

approve/deny additionally print in the past tense with wording identical to a real run, so nothing in the output signals that the change was applied:

$ npm install-scripts approve simple-git-hooks --dry-run
Approved simple-git-hooks:
  added [email protected]

Expected Behavior

With --dry-run, package.json must not be written and the pending change should be reported (ideally using the conditional wording prune already uses). Only a run without --dry-run should persist the allowScripts entry.

Steps To Reproduce

bash
mkdir npm-dryrun && cd npm-dryrun
printf '{"name":"repro","version":"1.0.0","private":true}\n' > package.json

# any dependency declaring an install script (here: postinstall)
npm install simple-git-hooks --no-audit --no-fund

shasum -a 256 package.json          # sha256sum on Linux
npm install-scripts approve simple-git-hooks --dry-run
shasum -a 256 package.json          # hash differs
cat package.json                    # allowScripts was written

Observed:

hash before : b814c0fb02fe7092...
$ npm install-scripts approve simple-git-hooks --dry-run
Approved simple-git-hooks:
  added [email protected]
hash after  : 848398be17e30c37...
allowScripts: {"[email protected]":true}

deny writes the same way:

hash before : 2db672d19eb5be80...
$ npm install-scripts deny simple-git-hooks --dry-run
Denied simple-git-hooks:
  removed-pinned-allow [email protected]
  added simple-git-hooks
hash after  : fb4ddceb2d9e7890...
allowScripts: {"simple-git-hooks":false}

Controls, same npm binary, both leave the file untouched:

bash
# sibling subcommand in the same command family
$ npm install-scripts prune --dry-run
Would remove 1 unused allowScripts entry:
  [email protected] (package not installed)
# hash unchanged

# unrelated command
$ npm install --dry-run lodash
# hash unchanged

Root cause

writePolicyChanges() in lib/utils/allow-scripts-cmd.js never consults the flag, unlike runPrune() in the same file:

javascript
// lib/utils/allow-scripts-cmd.js:275
async writePolicyChanges (groups) {
  const pin = this.npm.config.get('allow-scripts-pin') !== false
  // ... no `dryRun` is read anywhere in this method
  if (updated !== existing) {
    pkg.update({ allowScripts: updated })
    await pkg.save()          // :302 — written unconditionally
  }
  this.printSummary(summary)  // :305 — always past tense
}

Compare with the prune path, where the contract is asserted by a comment and then implemented:

javascript
// lib/utils/allow-scripts-cmd.js:332
// package.json (never `.npmrc`/CLI policy); `--dry-run` reports without writing.
async runPrune (args) {          // :333
  const dryRun = !!this.npm.config.get('dry-run')   // :341
  if (removed.length > 0 && !dryRun) {              // :371
    await pkg.save()                                // :376
  }
  this.printPruneSummary({ removed, dryRun })       // :380
}

printSummary() (:308) likewise has no dry-run branch, while printPruneSummary() (:383) selects `${dryRun ? 'Would remove' : 'Removed'} ...` (:394).

Suggested fix, matching the existing prune behavior: read dryRun in writePolicyChanges, skip pkg.save() when it is set, and give printSummary the conditional wording that printPruneSummary already uses.

Environment

  • npm: 12.0.2 — also reproduced on 11.19.0
  • Node.js: v24.21.0
  • OS Name: macOS 26.6.2 (Darwin 25.6.0), arm64
  • System Model Name: Mac16,9
  • npm config:
ini
; "builtin" config from .../npm/npmrc
prefix = "/opt/homebrew"

; node bin location = .../bin/node
; node version = v24.21.0
; npm version = 11.19.0