DEV Community

Cover image for Deno 2.6's minimum dependency age flag ignores year and month durations
Alex Georgiev
Alex Georgiev

Posted on AI-assisted

Deno 2.6's minimum dependency age flag ignores year and month durations

Deno 2.6 shipped in December with a feature aimed squarely at npm supply-chain attacks: you can now tell deno install and deno add to refuse any dependency that was published too recently. The idea is that a malicious package version usually gets pulled or reported within hours or days, so if you only ever install dependencies that have survived a waiting period, you dodge most of that class of attack.

I installed Deno 2.9.6, the current stable release that carries this feature, and spent an afternoon testing it against real npm packages. It works, mostly. It also has a silent failure mode that undoes the protection completely if you configure it the way the CLI's own syntax appears to allow.

The flag, and what it actually did

The relevant flag is --min-dep-age, and it takes minutes, an ISO-8601 duration, or an absolute date:

--min-dep-age <VALUE>  (Unstable) The age in minutes, ISO-8601 duration or
RFC3339 absolute timestamp (e.g. '120' for two hours, 'P2D' for two days,
'2025-09-16' for cutoff date, '2025-09-16T12:00:00+00:00' for cutoff time,
'0' to disable)
Enter fullscreen mode Exit fullscreen mode

I picked 20 packages that show up in most real JavaScript projects — react, vite, typescript, express, eslint, webpack, and so on — and ran a single deno add for all of them, once with no restriction and once with --min-dep-age=P30D (30 days). Same project, same lockfile-free start, same command, only the flag changed.

package no restriction with 30-day minimum
axios 1.20.0 1.19.0
eslint 10.10.0 10.8.1
jest 30.5.1 30.4.2
next 16.3.5 16.3.1
react 19.3.0 19.2.8
rollup 4.63.3 4.62.4
uuid 14.0.2 14.0.1
vite 8.3.0 8.2.1
vitest 5.0.1 4.1.10
vue 3.5.42 3.5.41
webpack 5.111.0 5.109.2
zod 4.6.5 4.4.3

Twelve of the twenty packages resolved to an older version under the 30-day policy. The other eight — chalk, commander, dayjs, esbuild, express, lodash, prettier, typescript — didn't change, because their latest release already happened to be more than 30 days old.

The one that stood out was vitest. Its actual latest version, 5.0.1, is 1.8 days old at the time I ran this. The newest version that clears a 30-day bar is 4.1.10 — a full major version behind. deno add npm:vitest --min-dep-age=P30D on a fresh project pins you to last year's major release without any error, warning, or indication that a newer major version exists. If you didn't know the policy was active, you'd file a confused bug report about vitest 5 "not being available".

Timing overhead was negligible: 10.2 seconds for the unrestricted batch install of 20 packages, 10.6 seconds with the age filter applied, run once each on a fresh Docker container with a cold cache. The filtering itself costs almost nothing; it's a metadata comparison against data npm already returns.

The bug: years and months don't apply the filter at all

The CLI help text only ever shows day-based examples (P2D), but ISO-8601 durations also support months and years, and the flag description doesn't say those are unsupported. So I tried them.

$ deno add npm:zod --min-dep-age=P7D    # 7 days
Add npm:zod@4.6.1   (published 7.3 days before the run)

$ deno add npm:zod --min-dep-age=P30D   # 30 days
Add npm:zod@4.4.3   (published 135.9 days before the run)

$ deno add npm:zod --min-dep-age=P1M    # 1 month
Add npm:zod@4.6.5   (published 3.2 days before the run — the true latest)

$ deno add npm:zod --min-dep-age=P2Y    # 2 years
Add npm:zod@4.6.5   (still the true latest)

$ deno add npm:zod --min-dep-age=P100Y  # 100 years
Add npm:zod@4.6.5   (still the true latest)
Enter fullscreen mode Exit fullscreen mode

Days and weeks work correctly — P30D and P4W both correctly restricted the install. Plain minutes work. Absolute dates work; I confirmed --min-dep-age=2024-01-01 correctly forced a much older 3.22.4. But the instant a duration contains a month or year component, even a zero one (P0Y1M0D, P1Y0M0D), the whole restriction is dropped and Deno installs the actual latest version, as if --min-dep-age had never been passed. I also tried adding a time component (P1DT0H), which broke the same way. Exit code is 0 in every case. Nothing is printed to stderr. I checked at --log-level=debug too — the debug log shows the resolver picking the version and nothing about a skipped or rejected candidate.

If your organisation's policy is naturally expressed in months ("nothing newer than one release cycle") or years, and you write that into deno.json as "minimumDependencyAge": "P6M", you get zero protection and no way to notice, short of doing what I just did and comparing installed versions against the registry by hand.

Config key versus CLI flag

While confirming that, I found a smaller inconsistency. The flag on the command line is --min-dep-age. The equivalent key in deno.json is not minDepAge — it's minimumDependencyAge, spelled out in full:

{
  "minimumDependencyAge": "P30D"
}
Enter fullscreen mode Exit fullscreen mode

I tested minDepAge as a config key first, assuming it would mirror the flag name, and it was silently ignored (again, no warning — just an unrestricted install). The Deno blog announcement does use minimumDependencyAge for the config key, so that part of the docs is accurate; it's the mental shortcut from flag name to config name that trips you up.

The policy doesn't apply to an existing lockfile

I locked npm:zod@4.6.5 with no restriction, deleted node_modules, then ran deno install --min-dep-age=P30D against the same project and lockfile.

$ deno install --min-dep-age=P30D
Download https://registry.npmjs.org/zod/-/zod-4.6.5.tgz
+ npm:zod 4.6.5
Enter fullscreen mode Exit fullscreen mode

It reinstalled 4.6.5 without complaint, even though that version is only 3.2 days old and well inside the 30-day window I'd asked for. The age check runs at resolution time — when you add a dependency or update the lock — not on every install. That's a reasonable design for reproducible builds, but it means setting --min-dep-age in your CI install step gives you nothing if the lockfile was already committed by someone who didn't have it configured. The protection lives at the point a version gets chosen, not at the point it gets installed.

Lifecycle scripts: the part that does work as documented

Deno 2.6 also replaced deno install --allow-scripts with deno approve-scripts, a more granular way to permit npm postinstall hooks. I tested it against simple-git-hooks, a real package with a postinstall script, on a project with "nodeModulesDir": "auto" set.

Installing without approval:

Add npm:simple-git-hooks@2.14.0

╭ Warning
│  Ignored build scripts for packages:
│  npm:simple-git-hooks@2.14.0
│
│  Run "deno approve-scripts" to run build scripts.
╰─
Enter fullscreen mode Exit fullscreen mode

No script ran. Approving it explicitly by name, non-interactively:

$ deno approve-scripts npm:simple-git-hooks
Approved npm:simple-git-hooks
Initialize simple-git-hooks@2.14.0: running 'postinstall' script
Ran build script npm:simple-git-hooks
Enter fullscreen mode Exit fullscreen mode

And it wrote the approval to deno.json:

"allowScripts": ["npm:simple-git-hooks"]
Enter fullscreen mode Exit fullscreen mode

This part matched the documentation exactly: scripts blocked by default, a specific command to unblock them, and a persistent, readable record of what you approved. No complaints here.

deno audit couldn't reach the registry in my setup

The other headline feature in 2.6 is deno audit, which checks your dependency graph against npm's advisory database. I installed a known-vulnerable lodash@4.17.15 and ran it:

$ deno audit
error: error sending request for url (https://registry.npmjs.org/-/npm/v1/security/advisories/bulk):
client error (Connect): invalid peer certificate: UnknownIssuer
Enter fullscreen mode Exit fullscreen mode

My environment sits behind a TLS-inspecting proxy, so my first assumption was that this was purely my setup's fault and not worth reporting. But I checked it properly before writing it off. I ran a two-line Deno script that called fetch() on that exact same URL, in the exact same container, with the exact same certificate configured — it returned a 200 and the advisory JSON without any problem. The plain npm installer in the same run also talked to registry.npmjs.org over the same connection settings without issue. Only deno audit's own HTTP client failed the handshake. That points to deno audit using a separate client that doesn't pick up the same certificate trust configuration as the rest of the CLI, which matters anywhere a corporate or CI proxy terminates TLS — exactly the kind of environment that would also want dependency auditing.

What I got wrong on the way

My first read on the deno audit failure was "proxy problem, not interesting, move on." I nearly left it out of the post. What changed my mind was remembering the instruction to distrust a null result before trusting it — a "no difference" or "doesn't work" result is exactly the kind of thing a broken harness produces. So instead of accepting the error message at face value, I isolated the one variable that mattered: same host, same certificate, same container, different code path in the same binary. That test is what turned "my proxy is awkward" into "this specific subcommand behaves differently from the rest of the CLI," which is a much more useful thing to tell a reader.

Run it yourself

This needs Docker and outbound access to registry.npmjs.org.

mkdir zod-age-test && cd zod-age-test
echo '{}' > deno.json

# baseline, no restriction
docker run --rm -v "$PWD":/work -w /work denoland/deno:latest \
  deno add npm:zod

rm deno.json deno.lock; echo '{}' > deno.json

# 7-day minimum, applies correctly
docker run --rm -v "$PWD":/work -w /work denoland/deno:latest \
  deno add npm:zod --min-dep-age=P7D

rm deno.json deno.lock; echo '{}' > deno.json

# 1-month minimum, silently ignored on Deno 2.9.6
docker run --rm -v "$PWD":/work -w /work denoland/deno:latest \
  deno add npm:zod --min-dep-age=P1M
Enter fullscreen mode Exit fullscreen mode

Compare the resolved version number in deno.json after each run. The P7D run should pin an older version than the plain run; the P1M run should match the plain run exactly, which is the bug. If you're behind a TLS-inspecting proxy, add --network host and mount your CA bundle with -e DENO_CERT=/path/in/container -v /path/on/host:/path/in/container or the commands above will fail before they reach the registry at all.

What to do with this

If you're adopting --min-dep-age or minimumDependencyAge today, express your policy in days or weeks, not months or years, and check the resulting deno.json/deno.lock diff after any deno add to confirm the version actually moved. Don't rely on the flag to protect dependencies already sitting in a lockfile — it only acts when a version is first chosen. And if you're behind any kind of TLS-inspecting proxy, test deno audit specifically before you put it in a CI gate; it may fail differently from every other network call Deno makes. This is still an unstable feature by Deno's own labelling, so none of this should be surprising in isolation — but it is the sort of thing you want to find out before you write the policy into a deno.json that a team is going to trust.

Top comments (0)