Dependency Cooldowns Took Me Twenty Minutes
March 28, 2026 · 947 words · 5 min read
The litellm attack should be a wake-up call.
This week Callum McMahon at FutureSearch published the full transcript of how he discovered a supply chain attack inside litellm on PyPI. The compromised version (1.82.8) uploaded directly to PyPI with no matching GitHub tag and stole SSH keys, cloud credentials, Kubernetes tokens, .env files, and crypto wallets. It exfiltrated everything encrypted to a domain designed to look like legitimate litellm infrastructure. Then it attempted persistence via a fake systemd service, lateral movement into Kubernetes clusters, and as a side effect, a fork bomb that brought down his laptop with 11,000 processes. The whole thing was live on PyPI for barely an hour before it got reported and yanked.
The transcript is worth reading in full, for the timeline, which is just nuts. Poisoned package uploaded at 10:52 UTC. Pulled as a transitive dependency at 10:58. Six minutes! McMahon wasn't even installing litellm directly, Cursor triggered a MCP server that depended on it, uv resolved the latest version, and then the payload was running before anyone knew it existed.
I read it and immediately thought about my own setup. I run Next.js projects on Vercel. I also have Dependabot configured on most of my repositories. I use uv and npm daily. I have .env files with API keys sitting in every project directory. I am exactly the person this attack was designed to hit, someone who trusts the ecosystem enough to let dependencies update without thinking about it.
So I spent twenty minutes doing the thing I'd been meaning to do since I read William Woodruff's post on dependency cooldowns back in November. The idea itself is dead simple, just don't install a package version the same day it was published. Most supply chain attacks get spotted and pulled within hours. If your tooling refuses to touch anything younger than a few days, the compromised version is almost certainly gone before it ever reaches your lockfile. It wouldn't have helped McMahon as the guy was patient zero, pulled the package six minutes after upload, but it would help everyone else.
npm shipped min-release-age in v11.10.0 back in February. One line in .npmrc is all it takes.
min-release-age=3dThat's it! Any npm install or npm update now skips versions published less than three days ago. It covers both direct and transitive dependencies, which is super important because attacks like litellm rarely target your top-level packages, they almost always go out of their way to target something deep in the tree that nobody's watching.
Three days is my default. Enterprise setups often go higher, I read that Snyk enforces 21 days, but that's overkill for me. For personal projects, three days is the right balance. The litellm attack was flagged and the package yanked within an hour. Three days gives the community plenty of time to catch something, and it's short enough that I'm not blocked waiting for a patch I actually need.
For pnpm projects the equivalent is minimumReleaseAge in .npmrc, which pnpm has supported since 10.16, months before npm finally caught up. Same idea.
On the Python side, things are not as clean. pip doesn't have a native cooldown mechanism yet. uv is what McMahon was using when he got hit, and it doesn't either. The best you can do today is pin your versions tightly and update deliberately, or use Renovate with minimumReleaseAge to gate automated PRs. This is a bit of a gap, but after seeing what happened with litellm, it feels like a pretty urgent one to fill. The Python ecosystem is huge and the attack surface is enormous, and the fact that we don't have a simple config option to add a cooldown is a problem. What happened with litellm is a good reminder that the next attack that's coming down the pipeline is inevitable. The JavaScript ecosystem moved on this faster than Python did, and we just got a very compelling argument for why Python needs to catch up.
The Dependabot side is where things get slightly more interesting. If you only set min-release-age in .npmrc, Dependabot doesn't know about it. It'll still open PRs for versions published hours ago, and then CI fails because npm refuses to install them. It's noisy and confusing! So you need to tell Dependabot to wait too.
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 3
semver-major-days: 7
semver-minor-days: 3
semver-patch-days: 3Major versions get a longer window because I'm reviewing those carefully anyway and a few extra days of community testing costs nothing. Security updates bypass the cooldown entirely, so if there's a known CVE with a published fix, waiting is the wrong move. That asymmetry is the correct default.
I also added cooldowns to GitHub Actions dependencies, because actions run with repo-level permissions and the blast radius of a compromised action is arguably worse than a compromised npm package:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7The part of McMahon's transcript that stuck with me most was how ordinary the infection path was. A tool restarted, a dependency got pulled, a transitive package resolved to the latest version, and then that's it, payload was executing within seconds, with uv doing exactly what it was supposed to do, just with a package that happened to be poisoned six minutes earlier.
Cooldowns don't solve this for patient zero. Nothing does, except maybe vendor lockdown or total air-gapping, neither of which is realistic for someone building side projects on a work evening. But for the other 99.99% of developers who would have pulled litellm 1.82.8 sometime in the following hours and days before it got flagged and PyPI yanked it, a three-day cooldown would have been a complete fix. The attack window would have closed long before the package ever reached their machines.
Twenty minutes! Two config files. One line in .npmrc, one block in dependabot.yml. If you're running anything on npm and you haven't set this up yet, the litellm transcript is your reason to do it today.