Minimum release age is necessary, but not enough
15 Jun 2026The most-recommended npm defence right now is a cooldown: refuse to install any version published more recently than a few days ago. pnpm shipped it as minimumReleaseAge, Aikido Safe Chain turns it on by default, and after the recent Shai-Hulud-style npm worms, it became the advice everyone repeats.
It is good advice. It closes the window these worms detonate in. But "set a cooldown" is being sold as if it were the whole answer, and it is not. A cooldown knows one thing about a package: how old it is. There are at least three reasons it is not enough on its own, and for those, age is the wrong question to ask.
What a cooldown does
A minimum release age refuses to resolve any version published more recently than your threshold. Set it to three days and a version published two hours ago does not get installed; the resolver falls back to the last version that has aged past the line.
It is one of the strongest controls against publish-and-detonate worms. Those worms compromise a maintainer account, publish a malicious version, and spread within hours. Some of the versions developers pulled were five minutes and around an hour old. A cooldown refuses to even resolve anything that fresh, so the bad version is never the one you install.
Keep the cooldown. Everything below is about what it leaves open, not a case against it.
Gap 1: the malicious version that aged in
A cooldown asks one question: is this version old enough? It does not ask: is this version bad?
Most of the time those line up, because worms detonate fast and get caught fast. Not all of the time. A version can sit past your threshold and still be malicious. The attacker who plants something and waits a week beats a three-day cooldown by definition. Once a version crosses your age line, the cooldown no longer filters it at all.
So you need a second axis: not "too new to trust" but "known to be bad." That is a malware check. In sandbox-node's case, the second gate is an OSV malware check: @jagreehal/sandbox-node queries the OSV database for the exact version an install would pull and refuses anything flagged as malware, even a version old enough to clear the cooldown:
sandbox --fail-on-advisory npm install # block a known-malicious version
sandbox init --preset strict # strict turns this on for you
The two gates are complementary. The cooldown catches too-new-to-trust; the malware check catches known-bad. A zero-day worm is too fresh for any advisory, so the cooldown stops it. A republished known-bad version has aged in, so the malware check stops it. Neither covers the other.
Gap 2: the version that turned bad after you installed it
This second gap defeats both gates at once. An advisory database only knows what has been reported so far. Most supply-chain compromises surface later: a version is published, looks clean, passes your cooldown, passes the malware check because nothing is filed yet, you install it, and days afterwards OSV files a MAL-... advisory for it.
At install time, every check you can run returns green. The package was old enough and not yet known-bad. The cooldown is irrelevant here because the version had already aged in. The install-time malware check is irrelevant because the advisory did not exist yet.
This is why sandbox-node also re-checks after the fact:
sandbox scan # re-query OSV for every version in your committed lockfile
sandbox scan re-queries OSV for the versions in your committed lockfile and exits non-zero if any installed package is now flagged as malware. Run it nightly, or fold it into a CI gate, and a dependency that turned out bad after you installed it gets caught on the next sweep instead of never. A point-in-time gate cannot do this; only re-checking can.
Gap 3: the one that catches you
The cooldown is blunt in the other direction too. It does not only block other people's fresh malware; it blocks your own fresh publishes and any legitimate newly released dependency you want. So teams tune it down, exempt their own scope, or turn it off on the repos where the friction bites, which are often the repos that pull the most.
That is not a bug in the cooldown, it is the nature of a time-based gate: the only knob is a number of days, and that one number trades safety against friction for every package at once. You end up either too loose to stop a patient attacker or too tight to ship. sandbox-node lets you exempt your own scope so the gate stays strict for everything else:
{ "install": { "minReleaseAgeDays": 7, "minReleaseAgeExclude": ["@myscope/*"] } }
But exempting a scope is still trusting it by name, which is the assumption a maintainer-account takeover breaks.
The layer that does not depend on timing or recognition
Each gate above depends on knowing something in advance. The cooldown depends on the bad version being fresh. The malware check depends on someone having filed an advisory. Your own blocklist depends on you already knowing. Every one of them comes down to recognising the package, reading the code, or timing the release, and a brand-new, unrecognised, plausibly-aged package slips past all three.
The control that does not depend on any of that is containment. If the install runs in a throwaway box that cannot see your SSH keys, cannot reach your npm token, and has no outbound route beyond the registry it needs to fetch packages, it does not matter whether the version was fresh, flagged, or recognised. The credentials are not in the box. Even if a script tries to exfiltrate, the default-deny egress allowlist gives it no general-purpose route out. The box is deleted afterwards and your installed dependencies stay.
That is the difference worth being precise about. A cooldown, a malware check, and a blocklist all try to answer "should this code run?" and they answer it from incomplete information, every time. Containment changes the question to "if it runs, what can it reach?" and answers that one in full, regardless of what the gates knew.
sandbox npm install # the install runs with no credentials, default-deny egress, IMDS blackholed
Be precise about the claim. sandbox-node does not have a magically better blocklist: its malware check has publish lag like any other, and it blocks known malware, not every vulnerability. What sets it apart is that it does not rely on the blocklist being complete. The cooldown and the OSV check are the cheap front line; containment is the backstop for everything they miss.
Use them together, not instead
This is not cooldown versus sandbox. sandbox-node runs the cooldown and the malware check and the retroactive sweep and the containment, behind one prefix and one config:
sandbox init --preset strict # 7-day cooldown + OSV malware check + canaries + containment
sandbox npm install
sandbox scan # later, catch what turned bad after install
A worm too fresh to be flagged hits the cooldown. A known-bad version old enough to clear the cooldown hits the malware check. A version that turns bad after you install it gets caught by the next sandbox scan. And the one that beats all three, fresh enough or obscure enough that nothing flagged it, runs in a box with nothing of yours to steal and nowhere to send it.
Bottom line
A minimum release age is necessary. It closes the worm window better than anything else, and you should turn it on. It is not sufficient, because it asks only how old a package is, and a patient attacker, a late advisory, and your own release schedule all make age the wrong question.
The fix is not a longer cooldown or a better blocklist. It is to stop betting everything on the gate being right and run the install where a wrong answer cannot reach your credentials or your host environment. If you want the full picture, the safe-installs series walks through why install-time code is a trust decision, what running it in a throwaway container buys you, and how to put sandbox in front of the commands you already run.