← All ideas

Package Managers Need to Cool Down

This post was requested by Seth Larson, who asked if I could do a breakdown of dependency cooldowns across package managers. His framing: all tools should support a globally-configurable exclude-newer-than=<relative duration> like 7d, to bring the response times for autonomous exploitation back into the realm of human intervention.

When an attacker compromises a maintainer's credentials or takes over a dormant package, they publish a malicious version and wait for automated tooling to pull it into thousands of projects before anyone notices. William Woodruff made the case for dependency cooldowns in November 2025, then followed up with a redux a month later: don't install a package version until it's been on the registry for some minimum period, giving the community and security vendors time to flag problems before your build pulls them in. Of the ten supply chain attacks he examined, eight had windows of opportunity under a week, so even a modest cooldown of seven days would have blocked most of them from reaching end users.

The concept goes by different names depending on the tool (cooldown, minimumReleaseAge, stabilityDays, exclude-newer) and implementations vary in whether they use rolling durations or absolute timestamps, whether they cover transitive dependencies or just direct ones, and whether security updates are exempt.

JavaScript

The JavaScript ecosystem moved on this faster than anyone else, with pnpm shipping minimumReleaseAge in version 10.16 in September 2025, covering both direct and transitive dependencies with a minimumReleaseAgeExclude list for packages you trust enough to skip. Yarn shipped npmMinimalAgeGate in version 4.10.0 the same month (also in minutes, with npmPreapprovedPackages for exemptions), then Bun added minimumReleaseAge in version 1.3 in October 2025 via bunfig.toml. npm took longer but shipped min-release-age in version 11.10.0 in February 2026. Deno has --minimum-dependency-age for deno update and deno outdated. Five package managers in six months, which I can't think of a precedent for in terms of coordinated feature adoption across competing tools.

Python

uv has had --exclude-newer for absolute timestamps since early on and added relative duration support (e.g. 1 week, 30 days) in version 0.9.17 in December 2025, along with per-package overrides via exclude-newer-package. pip shipped --uploaded-prior-to in version 26.0 in January 2026 with absolute timestamps, then added relative duration support in version 26.1. Poetry added solver.min-release-age in version 2.4.0, so all three major Python installers now have it.

Ruby

RubyGems and Bundler now have a design proposal from maintainer Hiroshi Shibata: a cooldown setting in days that can be passed as a CLI flag, set via bundle config or the BUNDLE_COOLDOWN environment variable, or attached per source in the Gemfile as source "https://rubygems.org", cooldown: 7. Hanging it on the source rather than on individual gems means private registries simply don't get a cooldown, so there's no exclude list to maintain, and --cooldown 0 is the escape hatch when you need a CVE fix immediately.

Separately, gem.coop, a community-run gem server, is running a cooldowns beta that enforces a 48-hour delay on newly published gems served from a separate endpoint. Pushing the cooldown to the index level rather than the client is interesting because any Bundler user pointed at the gem.coop endpoint gets cooldowns without changing their tooling or workflow at all.

Other ecosystems

Cargo has an RFC in progress and the registry-side infrastructure for cooldowns is stabilized in Cargo 1.94 (releasing March 5, 2026). Their approach sidesteps the exemption list problem entirely: instead of exempting packages from cooldowns, you explicitly opt in to a new version with cargo update foo --precise 1.5.10, which records the choice in your lockfile. No exclude list to remember to clean up later. In the meantime there's also cargo-cooldown, a third-party wrapper that enforces a configurable cooldown window on developer machines as a proof-of-concept.

Go has an open proposal for go get and go mod tidy, Composer has two open issues, and NuGet has an open issue though .NET projects using Dependabot already get cooldowns on the update bot side since Dependabot expanded NuGet support in July 2025. Dart's pub has an open issue proposing minimum_release_age in pubspec.yaml with an exceptions list, and the discussion there has branched into whether a version should also be skipped when a newer one was published shortly after it, on the theory that a quick follow-up release probably fixed something you don't want. Elixir's Hex has an open issue for a cooldown option on mix hex.outdated, and conda, which manages packages for R and other languages as well as Python, has an open issue proposing --exclude-newer.

Dependency update tools

Renovate has had minimumReleaseAge (originally called stabilityDays) for years, adding a "pending" status check to update branches until the configured time has passed. Depfu shipped a "reasonably up-to-date" strategy in June 2019 that holds new releases for anywhere from a few days to a month before opening a PR, pitched at the time as a way to cut PR volume rather than a supply chain defence. Mend Renovate 42 made a 3-day minimum release age the default for npm packages in their "best practices" config via the security:minimumReleaseAgeNpm preset, making cooldowns opt-out rather than opt-in for their users.

Dependabot shipped cooldowns in July 2025 with a cooldown block in dependabot.yml supporting default-days and per-semver-level overrides (semver-major-days, semver-minor-days, semver-patch-days), with security updates bypassing the cooldown. Snyk takes the most aggressive stance with a built-in non-configurable 21-day cooldown on automatic upgrade PRs. npm-check-updates added a --cooldown parameter that accepts duration suffixes like 7d or 12h.

Checking your config

zizmor added a dependabot-cooldown audit rule in version 1.15.0 that flags Dependabot configs missing cooldown settings or with insufficient cooldown periods (default threshold: 7 days), with auto-fix support. StepSecurity offers a GitHub PR check that fails PRs introducing npm packages released within a configurable cooldown period. OpenRewrite has an AddDependabotCooldown recipe for automatically adding cooldown sections to Dependabot config files. For GitHub Actions specifically, pinact added a --min-age flag, and prek (a Rust reimplementation of pre-commit) added --cooldown-days.

Still waiting

For Go, Bundler, Composer, Dart, Hex, and conda, cooldown support is still in discussion or only partially landed, which means you're relying on Dependabot or Renovate to enforce the delay. That covers automated updates, but nothing stops someone from running bundle update or go get locally and pulling in a version that's been on the registry for ten minutes. I couldn't find any cooldown discussion at all for Maven, Gradle, or Swift Package Manager. If you know of one, let me know and I'll update this post.

The feature also goes by at least ten different configuration names across the tools that do support it (cooldown, minimumReleaseAge, min-release-age, npmMinimalAgeGate, exclude-newer, stabilityDays, uploaded-prior-to, min-age, cooldown-days, minimum-dependency-age), which makes writing about it almost as hard as configuring it across a polyglot project.

Language vs. system package managers

On npm, PyPI, and RubyGems, running npm publish or gem push makes a package installable worldwide in seconds, and if Dependabot or Renovate happens to run in that window, the malicious code lands in a project without a human ever seeing it. All of the supply chain attacks William examined exploit this property, where publishing and distribution are the same act and nothing stands between a compromised maintainer account and thousands of downstream projects.

System package managers work differently because they separate those two things. When someone pushes a new version of an upstream library, it doesn't appear in apt install or brew install until a distribution maintainer has reviewed the change, updated the package definition, and pushed it through a build pipeline. Fedora packages go through review and koji builds, Homebrew requires a pull request that passes CI and gets merged by a maintainer. A compromised upstream tarball still has to survive that process before it reaches anyone's machine, and the people doing the reviews tend to notice when a patch adds an obfuscated postinstall script that curls a remote payload.

On Debian, even a compromised maintainer's upload lands in unstable first, then automigrates to testing after 2 to 10 days depending on urgency and availability of package tests, and stable only gets updates through a separate release process.

Cooldowns on the language package manager side are trying to retrofit something like that review window onto ecosystems that never had one, giving security researchers a few days to flag a malicious publish before automated tooling pulls it into lockfiles. Asking Homebrew or apt to add the same feature would mean delaying security patches through a process that already has human gatekeepers, which costs more than it saves.

The timestamp problem

pip's --uploaded-prior-to and npm's older --before flag originally only took absolute timestamps, and the discussion about adding relative duration support to pip reveals how these two modes serve different goals that happen to share implementation surface. An absolute timestamp pins your dependency resolution to a moment in time, so running the same install six months from now produces the same result. A relative duration like 7 days is the security version: a sliding window that moves forward with you, always excluding recent publishes regardless of when the build runs. pip added relative durations in 26.1, uv's --exclude-newer accepts both forms, and npm has both --before for absolute dates and min-release-age for relative durations. pnpm, Yarn, Bun, and Deno only accept relative durations.

The pip thread also got into the fiddly business of parsing duration strings. ISO 8601 durations (P7D) are unambiguous but nobody wants to type them, human-readable strings like 7 days are friendly but need a parser, and variable-length calendar units like months and years require knowing which month you're in to convert to a concrete number of days. uv went with ISO 8601 plus friendly strings but excluded months and years entirely, and pip went with relative durations in 26.1, which covers nearly every real use case without dragging in leap year arithmetic.

Even the question of what "seven days ago" means gets complicated when your CI server is in UTC, your developer laptop is in US Pacific time, and the registry timestamp uses whatever timezone PyPI's servers happen to be configured with. A few hours of timezone skew can determine whether a package published six days and twenty-two hours ago passes the cooldown check or not.

Which timestamp counts as "published" also varies: Dependabot's GitHub Actions cooldown uses the tag's commit date rather than the release publication date, so a tag cut 100 days ago but only released yesterday sails straight through a 7-day cooldown. Most language registries have a single upload timestamp so this doesn't come up, but anything sourced from git tags (GitHub Actions, Go modules, git submodules) has at least two candidate dates that can be months apart.

Whether the registry exposes a timestamp at all predicts adoption speed pretty well: the ecosystems that already had publish times in their index API moved fastest, because cooldowns become a pure client-side filter when the resolver can already see when each version landed. npm's packument has carried a per-version time field for years and PyPI added upload-time to the simple index via PEP 700 in 2022, so pnpm, uv, and pip could ship the feature without touching the server.

The npm field does come with a cost though, since time only appears in the full packument and not the abbreviated response most installers normally request, so enabling cooldowns means pulling multi-megabyte JSON files for popular packages instead of the slim variant. RubyGems' compact index doesn't carry timestamps at all, so the Bundler work depends on adding created_at to the compact index /info endpoint first, and changing a wire format that every Ruby install reads needs broader agreement than a client-side flag does.

← All ideas