Safeguard
Supply Chain

npm-check-updates: A Safe Dependency Upgrade Workflow

npm check updates (ncu) shows you every dependency with a newer version than your ranges allow. The tool is simple; the workflow around it is what keeps upgrades from breaking prod.

Priya Mehta
DevSecOps Engineer
7 min read

npm check updates — the npm-check-updates package, invoked as ncu — scans your package.json and lists every dependency with a version newer than your declared ranges allow, including the major bumps that npm outdated shows but npm update will never install. That is the whole tool: it edits version ranges in package.json and deliberately does nothing else. No installing, no testing, no judgment about whether the new major of your ORM will eat your weekend. Which means the safety lives entirely in the workflow you wrap around it. Here is one that scales from a side project to a monorepo, built around staged targets, lockfile review, and a test gate per risk tier.

What ncu does and pointedly does not do

Run without flags, it reports; with -u, it rewrites package.json:

npx npm-check-updates          # report what could be upgraded
npx npm-check-updates -u      # rewrite package.json ranges
npm install                    # actually resolve and install

The gap between npm update and ncu is version-range semantics. If you declare ^4.17.0, npm update will take you to the latest 4.x and stop, because that is what the caret permits. ncu proposes ^5.0.0 — it changes the contract, not just the resolution. That is precisely why it needs more ceremony than a routine npm update: every major bump is a claim that you have read the breaking changes, and the tool cannot make that claim for you.

ncu also does not touch your lockfile until you run npm install, does not run your tests, and does not know which of your dependencies are load-bearing. Treat its output as a to-do list, not a done list.

Stage upgrades by risk, not alphabetically

The single highest-leverage flag is --target, which caps how far ncu will reach:

ncu -u --target patch    # bug fixes only
ncu -u --target minor    # features, no declared breaking changes
ncu -u --target latest   # everything, majors included (default)

A staged cadence that works in practice:

  1. Weekly: patch and minor sweep. ncu -u --target minor, install, run the full test suite, merge. Semver minors are not guaranteed safe, but the breakage rate is low enough to batch.
  2. Monthly or quarterly: majors, one at a time. List them with ncu --target latest, then upgrade individually with a filter: ncu -u --filter typescript. One major per branch, its changelog actually read, its codemod actually run. Batching five majors into one PR guarantees that when something breaks, you get to bisect your own upgrade.
  3. Immediately, out of band: security advisories. Vulnerability-driven bumps should not wait for the sweep. Your scanner, not ncu, is the trigger here — ncu has no CVE awareness at all. Pairing it with an SCA tool such as Safeguard covers the gap: the scanner tells you which upgrade is urgent and whether the fixed version is actually reachable from your range, ncu mechanizes the range edit.

For riskier packages, --interactive gives you a checkbox picker, and --reject lets you fence off packages you never want auto-touched:

ncu -u --interactive --reject "react,react-dom,next"

Frameworks with their own upgrade tooling (Next.js codemods, ng update, React majors) should be excluded from generic sweeps and upgraded through their native path.

The lockfile diff is the review

After ncu -u && npm install, the meaningful diff is package-lock.json, and reviewing it is a supply chain control, not busywork. What to actually look for:

  • Packages you did not upgrade that changed anyway. Transitive resolution shifts are normal, but a new transitive dependency appearing — especially one with install scripts — deserves a minute of attention. npm diff or your platform's lockfile-diff view makes this readable.
  • Integrity hash churn without version churn. A changed hash for the same version means the tarball changed. That should essentially never happen on the public registry and is worth stopping for.
  • Registry URL changes. In monorepos with mixed private registries, a lockfile that silently repoints a scoped package at a different registry is a misconfiguration at best.

Two habits make this reviewable at all. First, keep upgrade PRs only about upgrades — no feature code riding along. Second, run npm ci in CI rather than npm install, so the lockfile you reviewed is byte-for-byte what the build uses. A lockfile-diff bot that flags new install scripts and new maintainers automates most of this; if you want a deeper treatment of picking automation here, the Safeguard Academy covers lockfile review patterns in its supply chain track.

Verify beyond the unit tests

ncu --doctor automates a first pass: it upgrades packages one at a time, runs your test command after each, and reverts the ones that fail:

ncu --doctor -u

It is a blunt instrument — it only knows pass/fail from your test script — but it converts "which of these 30 bumps broke the build" from an afternoon of bisection into a log file. For anything user-facing, add a smoke layer the unit suite cannot cover: build the app, boot it, hit the critical paths. Dependency upgrades are exactly the change class where a quick DAST pass against staging earns its keep, because behavior changes in middleware, parsers, and auth libraries express themselves in HTTP responses, not in type errors.

Version-pinning philosophy interacts with all of this. Teams split between exact pins (4.17.21) with automation doing all movement, and ranges (^4.17.21) with the lockfile pinning reality. Both work with ncu; what does not work is ranges without lockfile discipline, where npm install on a random Tuesday changes your dependency tree with no PR at all.

Monorepos and CI wiring

ncu handles workspaces natively:

ncu -u --workspaces        # every workspace package.json
ncu -u -w api -w shared    # selected workspaces

with --root controlling whether the root manifest is included. The staged-target strategy applies per workspace, and the lockfile review gets more important, not less — one hoisted resolution change can affect every package in the repo.

In CI, the useful automation is a scheduled job that opens the weekly minor-sweep PR automatically (ncu in a script, or Renovate/Dependabot configured with the same grouping rules), plus a required status check that the lockfile was generated by npm ci-compatible tooling. The goal state is that dependency updates are boring, frequent, and small — the security posture failure mode is the opposite one, where updates are so rare that each becomes a risky migration project, and known-vulnerable versions sit unpatched because "upgrades are scary here."

FAQ

What is the difference between npm outdated, npm update, and npm-check-updates?

npm outdated reports newer versions but changes nothing. npm update installs the newest versions allowed by your existing ranges and updates the lockfile. npm-check-updates rewrites the ranges themselves in package.json, including major bumps, and leaves installing to you.

Is it safe to run ncu -u on everything at once?

Mechanically yes, operationally no. Batch patch/minor updates freely behind a full test run, but take majors one per branch with the changelog read. --target minor for sweeps and --filter <pkg> for individual majors encode that split.

Does npm-check-updates know about security vulnerabilities?

No. It compares versions against the registry and nothing else. Use a vulnerability scanner to prioritize which upgrades are security-urgent, then use ncu to apply the range changes it recommends.

How does ncu --doctor mode work?

ncu --doctor -u upgrades dependencies one at a time, runs your test script after each bump, keeps upgrades that pass, and reverts ones that fail — a coarse automated bisection of a large upgrade batch. It is only as good as the test command you give it.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.