Cooldown does not fix the resolver
Bundler 4.0.13's cooldown filter refuses to resolve gem versions newer than N days, closing the supply-chain window while other defenses catch a bad release.
Bundler 4.0.13 ships a filter called cooldown that refuses to resolve to any gem version until it has been public for at least N days. The attack it targets is narrow: an account gets compromised, a malicious version ships, and every bundle install in the minutes that follow resolves straight to it. Holding new releases out of reach for a few days closes that window without anyone having to catch the attack in real time. The bet is that someone else does the catching during the wait.
That framing is worth keeping in mind, because cooldown does not verify anything. A gem that ages past your window and is still malicious resolves exactly as before. What cooldown buys is time for the registry’s other defenses, and for the rest of the ecosystem, to notice a bad version before your resolver reaches it. It is an opt-in delay on top of an unchanged resolver, not a replacement for one.
How it works
Cooldown reads the per-version created_at timestamp that rubygems.org’s v2 compact index now serves. During resolution, any version newer than the window is passed over in favor of one that has aged past it. The feature is unset by default, so a project that does nothing keeps resolving to the newest versions.
The timestamp dependency is the important caveat. A version whose source does not expose created_at - older gem servers, historical entries from before the v2 cutover, private registries still on the v1 format - is treated as outside the window and stays resolvable. Cooldown only holds back versions it can prove are too new, and it never blocks resolution silently. That cuts both ways: it will not filter a malicious version served from a registry that publishes no timestamps, but it also will not wedge your build against one.
Resolution is the only place it acts. An existing Gemfile.lock is honored as-is, so adding a cooldown never disturbs versions you have already locked. It takes effect when there is no lockfile yet (bundle install) or when you re-resolve against one (bundle update).
Setup
If you are on an earlier Bundler, update in place and pin the same version so the whole team moves together:
$ gem update --system # or: gem install bundler -v 4.0.13
$ bundle update --bundler=4.0.13
Then declare the window on your source. Committing it to the Gemfile is the right default for most teams, because every developer and CI run then enforces the same policy with no extra setup:
source "https://rubygems.org", cooldown: 7
gem "rails"
gem "puma"
The same value can be applied across every source through configuration or the environment instead:
$ bundle config set cooldown 7 # this project, stored in .bundle/config
$ bundle config set --global cooldown 7 # every project for this user
$ BUNDLE_COOLDOWN=7 bundle install # no lockfile yet, e.g. a fresh CI checkout
Or pass --cooldown for a one-off run of install, update, add, or outdated. When more than one is present they resolve in a fixed order: command-line flag beats the configuration setting, which beats a per-source cooldown: in the Gemfile. The value is always a non-negative integer number of days; a string, float, negative number, or array is rejected with an InvalidOption error.
Mixing sources and the escape hatch
The flag and the setting apply uniformly to every source, so the per-source keyword is what you reach for when policy differs by registry - cooling down public gems while trusting an internal one you operate:
source "https://rubygems.org", cooldown: 7
source "https://gems.internal.example.com", cooldown: 0 do
gem "internal-tool"
end
cooldown: 0 keeps the private source permanently exempt while public gems still age. One thing to watch: a command-line --cooldown N overrides every source for that run, including the exempt one.
Passing 0 disables cooldown for a run, which is the case that matters most. When a fix lands for an actively exploited 0-day, or a disclosure tells you to upgrade now, the freshest release is exactly the one you want. --cooldown 0 lets you take it without removing the policy for everyone else:
$ bundle install --cooldown 0
$ BUNDLE_COOLDOWN=0 bundle update rails
You will also hit this when every candidate version of a gem is still inside the window. Rather than quietly fall back to a too-new release, Bundler stops, reports how many versions the cooldown excluded, and points you at --cooldown 0 to proceed. And bundle outdated is cooldown-aware, annotating versions that are newer but still waiting with the days left before they resolve:
$ bundle outdated --cooldown 7
Gem Current Latest Requested Groups
aws-partitions 1.1251.0 1.1256.0 (cooldown 3d) = 1.1251.0 default
That keeps “up to date” distinct from “deliberately waiting,” which is the distinction that makes a cooldown policy legible to the person reading the diff.
Where it sits
Cooldown only works because rubygems.org now publishes created_at through its v2 compact index, delivered via a dual-write migration and a flagged cutover (v2 is an internal format version, not a public API change). The client-side filter is thin on top of that groundwork.
It is one layer among several the registry is building. Gem contents are validated at push time. Logins are checked against Have I Been Pwned so compromised passwords cannot be reused. A dedicated team runs AI-assisted vulnerability scanning against the most critical gems, backed by Alpha Omega and Anthropic. Trusted publishing and mandatory 2FA already raise the bar for who can push a release at all. Cooldown covers the gap between a bad version shipping and those defenses catching it, and it only pays off if you are resolving against a registry where that catching actually happens.
Keep Reading
trusting-trustA trusting-trust backdoor built from GNU strip
A tampered copy of GNU strip in the NixOS binary seed carried a trusting-trust backdoor into almost every binary of a complete graphical installer.
data breachA February 2025 migration exposed DigiCert customer records online
A February 2025 server migration exposed a DigiCert internal system, and a self-registered account read archived customer data and identity documents.
OpenSSHLong Passwords Bypass Alpine SSH Authentication
OpenSSH 10.6p2 fixes a critical password authentication bypass affecting Alpine Linux and other potentially vulnerable musl systems.
Latest on the Wire
Full wire →- Bending Spoons Destroys Harvest With 10x Price Hike and LLM SupportHacker News
- CISA Flags SonicWall MFA Bypass Amid Exploitation ReportsThe Hacker News
- Claude Opus Ports 3D Models from Three.js to Vanilla WebGLSimon Willison
- Collapsing AI PR descriptions with HTML details tagsSimon Willison
New signal daily · RSS
Stay in the loop
New writing delivered when it's ready. No schedule, no spam.