npm / npm/cli

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

Open Beginner friendly
#9,992 0 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

  • 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:
  not-installed-pkg@9.9.9 (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 simple-git-hooks@2.14.0
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
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 simple-git-hooks@2.14.0
hash after  : 848398be17e30c37...
allowScripts: {"simple-git-hooks@2.14.0":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 simple-git-hooks@2.14.0
  added simple-git-hooks
hash after  : fb4ddceb2d9e7890...
allowScripts: {"simple-git-hooks":false}

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

# sibling subcommand in the same command family
$ npm install-scripts prune --dry-run
Would remove 1 unused allowScripts entry:
  not-installed-pkg@9.9.9 (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:

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

// 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:
; "builtin" config from .../npm/npmrc
prefix = "/opt/homebrew"

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

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 in lib/utils/allow-scripts-cmd.js by reading writePolicyChanges() and comparing it with runPrune(), then trace how approve and deny reach that write path. Reproduce the approve and deny commands with --dry-run and verify that package.json remains unchanged and the output reports the pending changes rather than completed ones.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
cli, devtools
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
84/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.