`npm diff` runs `prepare` scripts while comparing local directories
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 10.1k
- Forks
- 4.7k
- Avg merge
- 2d 2h
- Merged PRs (30d)
- 19
Description
Summary
When both --diff operands are local package directories, npm diff packages those directories through libnpmdiff and pacote. Pacote runs each directory package's prepare lifecycle unless --ignore-scripts is set. A command that appears read-only can therefore modify files or perform other actions declared by the selected package before showing the diff.
I am reporting this as a command behavior/UX issue, not as a security or bounty vulnerability. The user must run npm in, or explicitly select, the local project content.
Pre-submission checks
- I searched existing issues and did not find a report covering this same
npm diffand localprepare-script behavior. - I reproduced this with npm 12.0.2, the current latest release according to the npm registry at the time of this update.
- This is not a request to update a dependency solely to address a CVE.
Environment
- npm 12.0.2
- Node.js 26.5.0
- Windows
- Observed source revision: c9876d7ea7150b0702e4151210b9fa1a8dbc7fbf
Safe reproduction
Create left/package.json:
{
"name": "prepare-repro",
"version": "1.0.0",
"scripts": { "prepare": "node prepare.js" }
}
Create left/prepare.js:
require('node:fs').writeFileSync('prepare-ran.txt', 'ran\n')
Create left/index.js containing:
module.exports = 'left'
Create right/package.json:
{ "name": "prepare-repro", "version": "1.0.1" }
Create right/index.js containing:
module.exports = 'right'
From the temporary parent directory, run:
npm diff --diff=./left --diff=./right
After completion, left/prepare-ran.txt exists and is included in the computed diff because prepare ran before the package file list was created.
Control case:
npm diff --ignore-scripts --diff=./left --diff=./right
After removing the marker, the control command does not recreate it.
Expected behavior
Preferably, npm diff should not execute lifecycle scripts by default because it is an inspection command. If generated build artifacts must be compared, an explicit opt-in could retain that behavior. At minimum, the documentation and terminal output should clearly warn that local and Git operands may execute prepare.
Relevant flow
- lib/commands/diff.js:55-62 passes normal flat options to libnpmdiff.
- workspaces/libnpmdiff/lib/index.js:35-39 obtains tarballs for both manifests.
- pacote/lib/dir.js:33-59 runs prepare when present and not ignored.
- pacote/lib/dir.js:74-82 runs preparation before generating the package file list.
Impact and limitations
- Inspection can have surprising side effects and can alter the package before the diff is calculated.
- Scripts run with the invoking user's permissions, as normal npm lifecycle scripts do.
- --ignore-scripts prevents the behavior.
- The demonstrated case requires user-selected local project content; I did not establish an eligible npm bug-bounty attack path.
- Pacote may intentionally run prepare to match package materialization semantics. If so, documentation and warning improvements may be preferable to changing the default.
Suggested remediation
For npm diff, consider setting ignoreScripts: true when materializing directory operands unless the user explicitly opts in. Please retain a regression test for --ignore-scripts and verify ordinary registry comparisons remain unchanged.
I used AI-assisted tooling to support source-code review, hypothesis generation, duplicate searching, and report drafting. I reviewed the resulting analysis, verified claims against the cited source code, and reproduced behaviors locally where stated. No report was submitted or external system modified autonomously by the AI.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with lib/commands/diff.js:55-62, then trace directory materialization through workspaces/libnpmdiff/lib/index.js and pacote/lib/dir.js:33-59, 74-82. Reproduce the local-directory command and its --ignore-scripts control case; done means the chosen behavior is covered by a regression test while ordinary registry comparisons remain unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, nodejs
- Domain
- cli, devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 50/100