Don't Install Yesterday's Release
Most malicious package releases are caught within hours. A one-day waiting period before installing anything new costs almost nothing and sidesteps a whole class of supply-chain attacks. Here's how I set it up, and where the exceptions go.
6 min read
One of my project READMEs has a line that surprises people. It says, more or less: a new patch of the framework came out today, and we're deliberately not using it. The previous patch is the newest one we'll install.
That wasn't caution about the framework. It was a rule about time. Nothing gets installed until it has been public for at least 24 hours.
It's one of the cheapest security decisions I know of, and it's worth setting on every JavaScript project.
The attacks this is for#
Package registries have had a rough couple of years. The pattern repeats:
- An attacker gets control of a maintainer's account, often through phishing, or steals a publishing token.
- They publish a new version of a popular package with something malicious inside.
- Automated installs everywhere pick it up within minutes, because a new patch version matches the version ranges most projects use.
- Someone notices. The bad version is pulled. By then it's in a lot of builds.
In September 2025, a well-known maintainer was phished through a fake support email, and malicious versions of eighteen extremely popular packages were published, including chalk and debug. Together they're downloaded billions of times a week. The bad versions were live for roughly two hours before they were removed.
A week later, a self-spreading worm known as Shai-Hulud started moving through the registry. It stole credentials from machines that installed infected packages, then used any publishing tokens it found to push infected versions of other packages. Hundreds of packages were compromised before it was contained.
The common thread is speed. The damage happens in the window between "published" and "noticed". And that window is usually short. The pnpm team put it directly when they added a fix for this: in most cases, malicious releases are discovered and removed within an hour.
The fix is a waiting period#
If most bad releases are caught within hours, the simplest defense is to not install anything that new.
In pnpm, that's one setting, in minutes:
# pnpm-workspace.yaml
minimumReleaseAge: 1440 # 24 hours
With that set, pnpm won't resolve any version, direct or transitive, that was published less than a day ago. If the newest version of something is two hours old, you get the one before it. When the newer one has been around for a day, it becomes eligible.
The setting arrived in pnpm 10.16, in September 2025, right as those incidents were unfolding. In pnpm 11, a one-day delay became the default. It's still worth setting explicitly, for two reasons. It documents the intent in the repo. And when you set it yourself, pnpm treats it strictly: if nothing in the allowed range is old enough, the install fails instead of quietly falling back to a brand-new version.
Other ecosystems have similar ideas. Dependency update bots can be told to wait a number of days before proposing an upgrade. The principle is the same everywhere: let the rest of the world find the problem first.
What it costs#
Almost nothing, most of the time.
- You're a day behind on new releases. For nearly every dependency, that doesn't matter. Nobody's product depends on a patch that came out this morning.
- Fresh projects sometimes resolve one version back. That's the README line I mentioned. The framework had published a new patch that day, so the project pinned the previous one and noted why.
- Occasionally an install fails in strict mode because nothing in your range is old enough yet. That's the system working. You either wait or make a deliberate exception.
Compared with the cost of a stolen cloud credential or a compromised build, it's an easy trade.
Where the exceptions go#
There are real cases where you need something new right away. The main one is a security fix: a vulnerability is announced, the patched version is an hour old and you want it now.
pnpm handles this with an exclude list, by package name or pattern:
minimumReleaseAge: 1440
minimumReleaseAgeExclude:
- '@my-org/*' # our own packages, published by us
A few simple rules for the list:
- Your own packages can go on it. You published them, and waiting on yourself adds nothing.
- Urgent fixes go on it temporarily, with a comment saying why and a reminder to remove the entry.
- Nothing goes on it for convenience. "I want the new feature today" isn't a reason.
The exclude list is a small, reviewable file. Anyone reading the repo can see exactly where the rule is relaxed and why.
It's one layer, not the whole defense#
A cooldown is a good layer. It's not the only one worth having:
- Commit the lockfile, and install from it in CI. What you tested is what you ship.
- Be stingy with install scripts. Many attacks run in a package's install hook. Newer versions of pnpm don't run dependency build scripts unless you allow them, and I keep that list short.
- Watch for trust downgrades. pnpm can fail an install if a package that used to be published with stronger provenance suddenly isn't. That's a strong signal something changed hands.
- Keep secrets off developer machines where you can. If setup pulls secrets through a normal login instead of sharing key files, there's less for a stolen-credential worm to find. I wrote about that in One Command to a Running App.
- Use phishing-resistant multi-factor auth on any account that can publish.
None of these is complete on its own. Together, they turn a fast-moving attack from "it's in our build" into "we never installed it".
Why this matters more with agents#
I run a lot of agent-driven work now, and agents install packages. They add dependencies, upgrade them and run builds, often without a person looking at each step.
An agent will happily install the newest version of something that was published ten minutes ago. It has no instinct for "that's suspiciously fresh". A repo-level cooldown is a rule that applies to the agent exactly as it applies to me, without either of us remembering it.
It also helps with a quieter risk. Agents tend to suggest whatever version they saw most recently, or whatever the registry says is newest. A cooldown means that even an eager upgrade lands on a version the rest of the ecosystem has already had a day to examine.
That's the kind of guardrail I like best: set once in the repo, enforced by the tool and invisible until the day it matters.
How to turn it on#
For a pnpm project, today:
- Add
minimumReleaseAge: 1440topnpm-workspace.yaml. - Add your own scoped packages to
minimumReleaseAgeExclude, if you publish any. - Run an install and see what resolves differently. Usually nothing.
- Add a line to the README explaining the rule, so the next person isn't surprised when the newest version doesn't install.
For other package managers, check for an equivalent setting, or configure your dependency update bot to wait a day or more before opening upgrade PRs.
The takeaway#
Most supply-chain attacks on package registries live and die in a window of hours. A one-day cooldown on new releases keeps you out of that window for the price of being a day behind. It's a single line of config, it applies to people and agents alike, and the exceptions live in a short list anyone can review.
Related
- Delivery
One Command to a Running App
A setup guide with twenty steps is a list of ways to fail. I've started treating 'clone, run one command, see the app' as a feature of every codebase. Here's what goes into that command, and the design choices that make it possible.
Delivery · Infrastructure · Codebases6 min read
- Engineering
Fail Closed, Capture Before You Act
Two rules sit at the center of a governance runtime I've been building: when in doubt, the answer is no, and the record is written before anything happens. Here's what they mean in practice, and why they matter more as agents start taking real actions.
Architecture · Security · Agents7 min read
- Projects
A Terminal as a Portfolio
I rebuilt mg.design as an interactive terminal. You type commands, it answers. Here's why a hard constraint like "it's a terminal" made the design easier, not harder, and what it takes to make a fake shell feel real in the browser.
Front-end · Next.js · Prototyping5 min read