npm Dependency Remediation
Clear npm audit highs on a Node project (especially a fork pinned to an
upstream's dependency ranges) without silently breaking peer deps.
When to use
npm auditreports high/critical advisories.- Pre-release or maintenance pass: "clear the vulnerabilities".
- A fork (e.g. MagicMirror²) where
package.jsonranges are constrained by upstream — a force-bump can cascade into breakage.
Procedure
- Enumerate.
npm audit— note each advisory's severity AND whether the fix is in-range (npm printsTo address all issues, run: npm audit fix) vs force-required (fix available via npm audit fix --forcewith an out-of-range version likeundici@8.7.0). - In-range fix.
npm audit fix(NO--force). This bumps PATCH versions inpackage-lock.jsononly;package.jsonstays untouched when no new range is needed. Re-runnpm auditto confirm the cleared count. - Force-required: DO NOT auto-apply.
npm audit fix --forceinstalls OUTSIDE the stated range and can break the project's pinned peer deps (esp. on a fork). Instead research the minimal safe pin:npm view <pkg> versions --json+ the advisory's vulnerable range, pick the lowest version that resolves the advisory, assess peer-dep breakage, and report to the user for a decision (leave pending vs pin now). - Verify green — but dodge the dev-dep trap (below). Scope the test run.
- Confirm lockfile-only change.
git diff --name-onlyshould showpackage-lock.jsonand nothing structural.git diff package.jsonshould be EMPTY (audit fix didn't widen ranges). Grep the lockfile diff for the expected package names to prove only patch bumps landed, no top-level manifest churn.
Pitfalls
- Advisory lists decay — compare package names, not counts, before redoing work.
npm auditoutput is time-sensitive: NEW advisories are published continuously for the SAME installed versions, so an audit run weeks after a successful remediation will show vulnerabilities again — different packages/GHSAs, not a failed fix. Before concluding prior remediation didn't stick, diff the current advisory package names against the original task's list (e.g. July 16 list: ws/engine.io/undici; July 28 list: brace-expansion/js-yaml/markdown-it — all new, fix confirmed). Verify prior work withnpm ls <pkg>against the recommended pin, not by re-reading audit counts. - Lockfile-only remediation diffs sit UNCOMMITTED when the task says "no commit/push". A later session may have finished the fix and left it in the working tree.
git status --shortshowingM package-lock.jsonwith no other structural changes IS the prior session's verified work — commit it (after confirming tests were green) rather than re-running the remediation. Seen 2026-07-28 on HermesMirror: July 21 session left a 50+/20- lockfile diff uncommitted; it was the completed fix, not dirt. - Trust live
npm auditover task-context advisory lists. A request may cite "1 high undici + 3 moderate js-yaml/markdown-it" from stale intel; the live tree often shows a different set. Also, a named-package advisory can surface ONLY via its transitive sub-dep — e.g. an "undici advisory" appears in the audit asfast-uri(undici's URI parser), fixable in-range with no undici pin oroverridesentry needed. Before researching a force-pin for the named package, check whether its sub-deps already carry the advisory as an in-range fix. (Seen 2026-07-21 on HermesMirror: expected undici pin, actual fix was plainnpm audit fixbumping fast-uri 3.1.2→3.1.4.) - NODE_ENV=production (or
.npmrcomit[]=dev) makesnpm installskip devDependencies.node_moduleslooks populated (~8 bin entries) butvitest/eslintare absent →npm testfails withvitest: command not found, and a barenpm installreports "up to date" while still missing dev bins. FIX:npm install --include=dev. Capture this as the install command, not as "tests are broken". - Full
npm testis slow (300+ tests can exceed a 600s delegate timeout). For verification run module-scoped:node_modules/.bin/vitest run tests/unit/modules(or any sub-path). Use--reporter=dot --no-colorand pipe to a log file to watch progress live — a piped| tailbuffers everything until exit, so you can't tell if it's alive.pgrep -fl vitestconfirms the worker is running. - Don't dispatch a subagent to run
npm audit fix+ fullnpm testtogether — the long test run can hit the 600s cap and time out with partial work. Either run directly with a hard tool timeout, or scope tests. If you do delegate, separate the fast fix from the slow verification. - Pre-existing failures are not your regression. A suite with 1 failed test
- N file-load errors where the failures are in an unrelated module (e.g. a
require("node_helper")alias that doesn't resolve in this checkout, or a DOM-string assertion) is NOT caused by a lockfile patch bump — confirm by checking the failure is in code you didn't touch andpackage.jsonis unchanged.
- N file-load errors where the failures are in an unrelated module (e.g. a
Verification discipline
- After fix:
git diff --stat package-lock.jsonshows only expected bumps. npm audithigh count dropped to the force-required remainder(s) only.- Module-scoped test run passes (the code you touched / that matters).