deno audit is Deno's built-in command for checking npm dependencies against known vulnerabilities, and on the one thing it's built for it works well: point it at a project with old packages and it tells you, fast, exactly what's wrong with them. What I didn't expect was that three separate flags documented to act on those findings — --fix, --ignore, and --frozen-lockfile — would each leave the vulnerable state completely untouched when I tried them.
I'm running Deno 2.9.7, installed fresh today from the official installer. --fix is documented as a Deno 2.8 addition, so this is recent behaviour, not a years-old corner of the CLI.
I built a throwaway npm project with three packages I picked specifically because they're old enough to be vulnerable to things that are actually fixed upstream:
After deno install, deno audit pulled from the GitHub Advisory database and reported this, consistently across three separate runs:
| run | wall time | vulnerabilities found | |---|---|---| | 1 | 0.30s | 13 (0 low, 6 moderate, 5 high, 2 critical) | | 2 | 0.17s | 13 (0 low, 6 moderate, 5 high, 2 critical) | | 3 | 0.19s | 13 (0 low, 6 moderate, 5 high, 2 critical) |
Every advisory came with a severity, the affected version range, and a link to the GitHub advisory, for example:
That part held up exactly as advertised: fast, accurate against real, named CVEs, no network account or paid service involved. The advisories themselves also checked out against the public record — the lodash prototype-pollution and command-injection entries and the minimist prototype-pollution entries are long-documented issues with their own GHSA pages, and the semver ReDoS is the well-known CVE-2022-25883. Nothing fabricated, nothing stale.
This is the part of the post where most "I tried the new audit command" write-ups stop. I kept going, because catching a vulnerability and doing something about it are two different claims, and Deno's docs make promises about both.
Before testing the fix flags, I wanted to check the other supply-chain claim Deno makes: that npm lifecycle scripts don't run unless you approve them. I added core-js@3.39.0, a real, widely used package whose postinstall script does real work, to a fresh project and ran deno install:
The approval gets written to deno.json as an allowScripts array, so it's visible in a diff and in code review, not a silent one-time flag. This is the one claim in this post that worked exactly as documented, both halves of it: blocked by default, with a clear warning, and unblocked only by an explicit, recorded decision.
Deno's own documentation says --fix will "automatically upgrade vulnerable direct dependencies to a patched, semver-compatible version," skipping only major-version bumps and transitive dependencies with no clean upgrade path. I built the simplest possible case that should qualify: one direct dependency, one advisory, and a package.json range wide enough to reach the fix.
I pinned semver to exactly 7.5.1 first, installed it, then widened the constraint to ^7.5.1 in package.json without reinstalling — which is how a lockfile actually goes stale in practice: someone loosens a version range and doesn't immediately run install. 7.5.1 is vulnerable to a ReDoS (GHSA-c2qf-rxjj-qqgw, fixed in 7.5.2). To confirm a fix genuinely exists on that range, I installed ^7.5.1 fresh with no prior lockfile, and Deno resolved it straight to 7.8.5 — a clean, same-major, semver-compatible upgrade. So the fix is there, reachable, and exactly what --fix is documented to apply.
deno.lock was byte-for-byte identical before and after. --fix printed the exact same report it would have printed without the flag, with no message explaining why nothing changed, and exited 1 either way. I tried it two ways — as the --fix flag, and as deno audit fix, since the help text lists fix as a positional [ACTION] too — and got the same untouched lockfile both times. I also tried the messier three-package project from the first test, where lodash and minimist were pinned with no range at all (so genuinely unfixable in place) — that case not fixing anything makes sense. The single-package, open-range semver case does not.
--ignore is documented to "ignore advisories matching the given CVE IDs." I tried it against the semver advisory above, first with the GHSA id Deno itself prints (GHSA-c2qf-rxjj-qqgw), then with the actual CVE number for the same issue (CVE-2022-25883). Neither changed the output. The advisory box and the "Found 1 vulnerabilities" count were identical with and without the flag. For what it's worth, deno audit's own report never displays a CVE id anywhere, only GHSA links, which makes "matching the given CVE IDs" an odd claim to begin with.
--frozen-lockfile is documented to "error out if lockfile is out of date." I tested the most extreme version of out-of-date I could: I deleted deno.lock entirely and ran deno audit --frozen-lockfile against the same vulnerable package.json.
Exit code 0. A lockfile-integrity gate that's supposed to fail a CI job when the lockfile can't be trusted instead reported a clean bill of health, because with no lockfile to read it apparently has nothing to check and treats that as nothing to report, rather than as the exact condition it says it errors on.
I also noticed --level has a narrower version of the same problem: --level critical correctly limits which advisory boxes get printed, but the "Found 13 vulnerabilities, 0 low, 6 moderate, 5 high, 2 critical" summary line at the end still counts everything, filtered or not. Smaller issue, same pattern: the headline number doesn't reflect the flag you just passed.
None of this is theoretical for how these flags would actually get used. --frozen-lockfile is the kind of check you'd wire into a CI job specifically so a missing or stale lockfile fails the build loudly instead of merging quietly. --ignore is what you'd reach for the day a security team tells you a particular advisory is an accepted risk and you don't want it blocking every subsequent run. --fix is what you'd run on a Monday to clear a backlog of dependency advisories without reviewing each one by hand. All three are the kind of flag someone adds to a script once, stops reading the output of, and trusts from then on.
