March 28, 2026/827 words/4 min read
Dependency Cooldowns Took Me Twenty Minutes
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) was uploaded directly to PyPI with no matching GitHub tag. It stole SSH keys, cloud credentials, Kubernetes tokens, .env files, and crypto wallets, then sent the encrypted haul to a domain designed to look like legitimate litellm infrastructure. It also attempted persistence via a fake systemd service and lateral movement into Kubernetes clusters. As a side effect, a fork bomb 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 because the timeline is nuts. The poisoned package went up at 10:52 UTC and was pulled as a transitive dependency at 10:58. Six minutes. McMahon wasn't even installing litellm directly. Cursor triggered an MCP server that depended on it, uv resolved the latest version, and 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 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 sort of person this attack was designed to hit because I trust 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 is dead simple. Don't install a package version the same day it was published. Supply chain attacks often get spotted and pulled within hours. If your tooling refuses to touch anything younger than a few days, a compromised version has time to disappear before it reaches your lockfile. It wouldn't have helped McMahon because he was patient zero and pulled the package six minutes after upload. It would have protected anyone whose tooling tried to pull the package during the rest of the hour it was live.
npm shipped min-release-age in v11.10.0 back in February. One line in .npmrc is all it takes.
min-release-age=3dAfter that, any npm install or npm update skips versions published less than three days ago. It covers both direct and transitive dependencies. That matters because litellm reached McMahon as a transitive dependency, not one he installed himself.
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 package was yanked within an hour, so three days would have kept it off my machine without leaving me stuck waiting long for a patch I 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 versions tightly and update deliberately, or use Renovate with minimumReleaseAge to gate automated PRs. After seeing what happened with litellm, the lack of a simple cooldown option feels like an urgent gap. JavaScript tooling moved faster here. Python needs to catch up.
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. I gave Actions a longer cooldown.
- 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, and a transitive package resolved to the latest version. Within seconds, the payload was running. uv did exactly what it was supposed to do, only with a package that had been poisoned six minutes earlier.
Cooldowns don't solve this for patient zero. Maybe vendor lockdown or total air-gapping does, but neither is realistic for someone building side projects on a work evening. For anyone whose tooling tried to pull litellm 1.82.8 during the hour it was live, a three-day cooldown would have skipped it.
It took me two minutes! One line in .npmrc and 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.