Fix vulnerable dependencies with `dynv fix`: a tested walkthrough
Short answer: dynv fix reads what your project has installed, picks the lowest version of each package that clears every advisory affecting it, edits only manifests and lockfiles, runs your test command, and restores everything byte-for-byte if the tests fail. It never commits or pushes. Run dynv fix --dry-run first, then dynv fix --test "npm test", and reinstall afterwards so the scan sees the new versions.
dynv scan tells you what is wrong (CI gating). dynv fix is the next step: making the minimal change that fixes it, safely. This is a tested walkthrough on a small Node project with two vulnerable direct dependencies, run on 2026-11-01 with the current CLI and the real npm registry. The behaviour is documented in the vulnerability monitoring docs; we say where our results add detail.
The project
package.json pins two vulnerable versions, and a tiny test exercises both:
{ "name": "shop-api", "scripts": { "test": "node test.js" },
"dependencies": { "lodash": "4.17.15", "minimist": "1.2.0" } }
$ dynv scan
...
8 vulnerabilities (1 critical, 3 high, 4 medium) in 2 packages; 8 fixable.
1. Dry run
$ dynv fix --dry-run
would fix lodash 4.17.15 → 4.18.0 [minor; direct; npm] GHSA-29mw-wpgm-hmr9, GHSA-35jh-r3h4-6jhm, ... (6 advisories)
would fix minimist 1.2.0 → 1.2.6 [patch; direct; npm] GHSA-vh95-rmgr-6w4m, GHSA-xvch-5gv4-984h
0 fixed, 0 refused, 0 without a fix, 0 failed, 0 rolled back (dry run: nothing changed)
Each line shows the chosen target, its risk class (patch, minor or major), whether the package is direct or transitive, and every advisory the upgrade clears. For lodash, 4.18.0 is the lowest version that clears all six advisories, not the latest. --json prints the same plan as data (fixes[] with package, from, to, risk, status, files and commands) for tooling.
2. Your tests get a veto
$ dynv fix --test "exit 1" # a test command that fails
rolled back lodash 4.17.15 → 4.18.0 ...
rolled back minimist 1.2.0 → 1.2.6 ...
0 fixed, 0 refused, 0 without a fix, 0 failed, 2 rolled back
tests: `exit 1` exited 1 → every change was rolled back
(exit code 3)
We compared checksums afterwards: package.json and package-lock.json were byte-identical to before. The failing test is a stand-in; in real use pass your suite, for example --test "npm test". Per the docs, no repository script runs unless you pass --test, and package install scripts stay off unless you opt in with --allow-scripts.
3. The real fix
$ dynv fix --test "npm test"
fixed lodash 4.17.15 → 4.18.0 [minor; direct; npm] ...
fixed minimist 1.2.0 → 1.2.6 [patch; direct; npm] ...
2 fixed, 0 refused, 0 without a fix, 0 failed, 0 rolled back
tests: `npm test` exited 0
- "dependencies": { "lodash": "4.17.15", "minimist": "1.2.0" }
+ "dependencies": { "lodash": "^4.18.0", "minimist": "^1.2.6" }
git status shows only package.json and package-lock.json modified. Three things in that result are worth knowing:
- Pins became ranges. The exact pins
4.17.15and1.2.0were rewritten as caret ranges,^4.18.0and^1.2.6. If your policy is exact pins, review the diff and re-pin. - The lockfile resolved higher than the minimum. It recorded lodash 4.18.1 and minimist 1.2.8, the newest versions matching the new ranges, not the 4.18.0 and 1.2.6 the plan named. The check that matters is that nothing remains vulnerable.
- It edits the working tree only. It never commits, pushes, opens a PR or runs git; you review and commit.
4. Reinstall, then scan
Right after the fix, we ran dynv scan again and it still reported all eight findings. That is expected, and easy to trip over: the scan reads the installed node_modules first, and dynv fix changes the manifest and lockfile, not the installed tree. After npm ci:
$ npm ci --ignore-scripts
$ dynv scan
No known vulnerabilities in 2 packages (node_modules).
$ npm test
tests passed
In CI this order (fix, commit, then a clean npm ci and dynv scan on the pull request) is what you want anyway.
5. Majors are refused unless you say so
dynv fix limits itself to --max-risk minor by default; a major (or a 0.x minor) is not applied without --max-risk major. With [email protected]:
$ dynv fix --dry-run
refused (over --max-risk) jsonwebtoken 8.5.1 → 9.0.0 [major; direct; npm] GHSA-8cf7-32gw-wr33, GHSA-hjrf-2m68-5959, GHSA-qwph-4952-7xr6
$ dynv fix --dry-run --max-risk major
would fix jsonwebtoken 8.5.1 → 9.0.0 [major; direct; npm] ...
Without the flag the real run exits 1 (findings remain). A major bump can break your code; that is where tests, a human, or the experimental AI-assisted fixes come in.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Nothing left to fix (a dry run also exits 0) |
| 1 | Findings remain: no fix exists, a refused risk, a known-malicious package, or out of scope |
| 2 | An error |
| 3 | Your tests failed and everything was rolled back |
Safety properties (from the docs)
- Only registry versions are used; path and VCS specs are never written, and after the lockfile refresh every changed package must still resolve to the expected registry with an integrity hash where the format has one.
- It snapshots the plan’s manifest and lockfile paths, refuses any other changed file, and restores the snapshots on failure.
- Supported ecosystems: npm, PyPI, Go, crates.io, RubyGems and Packagist; limit with
--ecosystem. In automation (CI, orCI=true) Bundler and Poetry are refused unless you opt in, because locking them can evaluate repository or package build code. We tested npm only.
Automating it
The repository contains a GitHub Action with a fix mode, in two isolated jobs (a contents: read job runs dynv fix and uploads the patch; a separate write-token job applies only the planned files and opens one deduplicated dynv/fix pull request, never auto-merged by default). The Action is not published to the marketplace yet, so there is no uses: line to copy, and we did not run it for this post; see CI docs and the Action’s FIX.md in the repository. Until then, the same two commands in your own pipeline work: dynv fix --test "npm test" in a branch, then your normal PR step.
Limits
- The test run used one npm project and two direct dependencies; transitive-only vulnerabilities depend on the package manager’s override support.
dynv fixfixes known advisories by upgrading. It cannot fix a vulnerability that has no patched version, and it does not review your code.- A passing test suite is only as strong as the suite.
Facts last verified . Spotted an error? Tell us.