Cool your packages down
Supply chain attacks keep hitting. Two simple settings stop most of them: lockfiles and release-age cooldowns. Here's how to enable both across pip, uv, npm, pnpm and bun.
Another week, another supply chain attack. This time it was Mini Shai-Hulud1; 172 npm and PyPI packages hijacked in a few hours. Mistral AI's official SDK, TanStack Router, Guardrails AI, PyTorch Lightning. The malicious versions stayed live just long enough to catch CI pipelines before the registries pulled them.
I checked my own Python environment. Got lucky. But "got lucky" is not a security strategy, and these attacks are accelerating. There is a pattern to them and there is a defense, and the defense costs almost nothing.
The speed-run
Modern supply chain attacks look the same every time. An attacker phishes a maintainer, hijacks their npm or PyPI account and publishes a poisoned version of a package millions of people depend on. Within minutes, automated CI builds around the world pull the new version. Within hours, the registry pulls it back. The window is tiny; the blast radius is enormous.
In one notable case the axios package was compromised for about four hours2. That was enough to ship a remote access trojan into anyone whose CI ran during that window.
The standard advice (pin your dependencies, use lockfiles) is good advice. But it only solves half the problem. To see why, it helps to know exactly what a lockfile does and what it doesn't.
How lockfiles actually work
A lockfile records the exact version of every package you have installed, including transitive dependencies, along with a cryptographic hash of the file contents. Next time anyone runs install, the package manager pulls the same version and verifies that the bytes match the hash.
This protects you against one specific attack: someone tampering with a published version you have already approved. If you installed axios@1.13.0 and locked its hash, an attacker cannot republish a malicious 1.13.0 over the top; the hash will not match and your install will fail. Good.
What lockfiles look like in practice:
- JavaScript:
package-lock.json(npm),pnpm-lock.yaml(pnpm),bun.lock(bun),yarn.lock(yarn). All of them store SHA-512 integrity hashes for every resolved package. - Python: historically less standardised.
Pipfile.lock(pipenv),poetry.lock(poetry),pdm.lock(pdm) anduv.lock(uv) all do the same thing. Plainrequirements.txtdoes not lock by default, but you can pin hashes withpip install --require-hashesand the--hash=sha256:...syntax inside the file. uv and the others write hashes automatically.
When you run install:
- The package manager reads the lockfile.
- It downloads the exact versions listed.
- It hashes each downloaded artifact.
- It compares to the hash in the lockfile.
- If any hash differs, the install aborts.
Excellent for reproducibility. Excellent for catching tampered republishes. But notice what a lockfile does not protect you against: a new version of a package being malicious. The moment you (or Renovate, or Dependabot, or a teammate running npm install some-package) update a dependency, you are outside the lockfile's protection and back in the wild.
That is where the second defense comes in.
Minimum release age: the missing setting
Most supply chain attacks are caught within hours or days of publication. The malicious versions usually do not sit undetected for months; the community spots them fast. The xz utils backdoor3 is the sobering exception, lurking in production releases for roughly a month (on top of years of patient social engineering) before Andres Freund noticed it. But that level of patience is rare. So if you simply refuse to install anything published in the last few days, you skip the dangerous window for the great majority of incidents.
This is called minimum release age (also "dependency cooldown" or "release age gating"). It is a single config option that tells your package manager: don't resolve any version published more recently than N days ago.
According to research by Andrew Nesbitt4, a seven-day gate would have blocked roughly half of the 21 major supply chain incidents from the last decade. Cheap defense, big return.
Every major package manager added this in the last 12 months. The format and units differ per tool. The idea is the same.
pip (Python, v26.1+)
In /etc/pip.conf (system) or ~/.config/pip/pip.conf (user):
[global]
uploaded-prior-to = P7D
pip only accepts ISO 8601 durations or absolute timestamps. P7D means 7 days; P30D would mean 30. Friendly strings like "7 days" are not supported5.
uv (Python, v0.9.17+)
In pyproject.toml:
[tool.uv]
exclude-newer = "7 days"
uv is more forgiving and accepts friendly strings, ISO 8601 durations or RFC 3339 timestamps6. The [tool.uv] block lives alongside the rest of your project config, so it travels with your repo automatically.
npm (v11.x+)
In .npmrc:
min-release-age=7
npm counts in days. The canonical name is min-release-age per the official CLI docs7.
pnpm (v10.16+; default in v11)
In pnpm-workspace.yaml:
minimumReleaseAge: 10080
pnpm counts in minutes. 10080 minutes equals 7 days. pnpm 11 enables this by default at 1440 (one day), which is already a meaningful improvement; bumping it to a week is even safer8.
bun (v1.3+)
In bunfig.toml:
[install]
minimumReleaseAge = 604800
bun counts in seconds. 604800 seconds equals 7 days9.
Combining the two
Lockfiles and release-age gates protect against different failure modes. You want both.
- The lockfile says: "once we have vetted a specific version, no one can substitute a different one without us noticing."
- The release-age gate says: "when we update dependencies, never pull anything fresh enough to still be malicious."
Together they form a clean defense in depth:
- Day-to-day installs run from the lockfile. Hashes verified. No tampering possible.
- When you run an update (whether manually, via Renovate or via Dependabot), the package manager only considers versions older than your cooldown.
- The community has had a week to catch any malicious release. The version you pull is one the world has already audited by accident.
Neither is a silver bullet. A patient attacker could plant a sleeper version that stays clean for a month before activating. A hash check cannot catch a malicious first publish. But the combination raises the cost of attack dramatically and shrinks the window in which a compromise can reach you.
When you need to override
Sometimes you genuinely need the freshest version. A critical security patch lands, your CI breaks because of a regression in a dependency, or you are testing a new release. Every tool gives you an escape hatch:
- pip:
pip install --uploaded-prior-to=P0D <pkg> - uv: per-package overrides via
exclude-newer-package = { "some-pkg" = "2026-05-12" }under[tool.uv] - npm: pass
--min-release-age=0for the single command - pnpm: list the package under
minimumReleaseAgeExcludeinpnpm-workspace.yaml - bun: list the package under
minimumReleaseAgeExcludesinbunfig.toml
Use these sparingly. Every override is an exception you are consciously taking responsibility for.
What to do today
If you read this and your projects do not have a cooldown configured, here is the minimum viable hardening, in order of effort:
- Open your project's package manager config. Add the relevant line above. Commit it. That covers your team.
- Set it globally on your machine too (
~/.npmrc,~/.config/pip/pip.conf, etc.). That covers your one-off installs in random scratch folders. - Make sure you are actually using a lockfile. If you are on plain
requirements.txtwithout hashes, switch touvor add--require-hasheswith explicit--hashlines. - Document the cooldown in your CONTRIBUTING or README so future contributors do not paper over it the first time their CI complains about a "stale" package.
The next Mini Shai-Hulud is coming. The attackers are getting faster, not slower. Two settings, ten minutes of work, and a decade of supply chain incidents shrinks by half. Worth doing.
Footnotes
-
Coverage of the Mini Shai-Hulud campaign that hit 172 packages across npm and PyPI on May 11-12, 2026 TanStack, Mistral AI, UiPath hit in fresh supply chain attack. ↩
-
The axios compromise stayed live for about four hours before npm pulled it Axios compromised on npm. ↩
-
The xz utils backdoor (CVE-2024-3094) was planted in releases 5.6.0 and 5.6.1 by long-time co-maintainer "Jia Tan" and lived in distributed tarballs for roughly a month before Andres Freund discovered it in March 2024 XZ Utils backdoor. ↩
-
Andrew Nesbitt's analysis of how a 7-day cooldown would have blocked 11 of 21 major supply chain incidents Package managers need to cool down. ↩
-
Official pip CLI docs for
--uploaded-prior-to, including the ISO 8601 format requirement pip install reference. ↩ -
uv settings reference for
exclude-neweruv exclude-newer. ↩ -
npm CLI v11 config reference for
min-release-agenpm config reference. ↩ -
pnpm 11.0 release notes covering
minimumReleaseAgedefaults pnpm 11.0. ↩ -
Bun 1.3 release notes introducing
minimumReleaseAgeBun 1.3. ↩