We used to see one or two newsworthy stories a year about third-party dependencies containing malware, now we see one or two a month.
Thankfully, the security industry is getting a lot better at detecting these attacks. Many get caught within a few hours to a few days. Once an attack is identified, package registries like npm and PyPI can revoke the malicious versions, preventing additional downloads, thus limiting the window of opportunity for attackers.
In response to this onslaught of malicious package versions, many package managers have implemented cooldowns. Cooldowns govern how long a package distribution must have been available on the registry before it becomes eligible for installation.
A one week cooldown produces a surprisingly large security benefit with very little developer friction.
I was inspired by Yossarian's blog (and its sequel) to roll out cooldowns at Semgrep.
What is a Dependency Cooldown?
This blog is uv-centric since that is our main package manager, but the content should mostly apply to other package managers that have implemented cooldowns.
The examples below illustrate how adding a one week cooldown affects the version of a package that gets installed.
If you’re not familiar with Python development, pyproject.toml is a manifest file similar to package.json in JavaScript or a Gemfile in Ruby. These files govern which dependencies to install, and which versions are allowed.
A cooldown constrains which versions are eligible for installation by uv.
Example:
Your project uses litellm@1.80.2
1.80.2 - safe, released 5 weeks ago
1.80.6 - safe, released 3 weeks ago ✅
1.80.8 - malicious, released yesterday 😈
Without a cooldown
# pyproject.toml
dependencies = [
“litellm>=1.80,<2”
]
$ uv lock --upgrade
Updated litellm v1.80.2 -> v1.80.8 😈
With a cooldown
# pyproject.toml
dependencies = [
“litellm>=1.80,<2”
]
[tool.uv]
exclude-newer = "1 week"
$ uv lock --upgrade
Updated litellm v1.80.2 -> v1.80.6 ✅
Early learnings: Adding Cooldowns in Python
Claude Code is my go-to tool for quick iteration, exploring codebases, and learning about new problems like this. One caveat is the cooldown space is moving pretty quickly and the training data is often outdated, so I recommend pointing your agent to the relevant docs and also skimming them yourself so you can call BS.
I started rolling out cooldowns in late March, and have made updates to my tooling, as uv's cooldown implementation has evolved.
I started with our main app’s repo because it’s one I’m familiar with, it’s under active development, its manifest is complex enough to be interesting, and it would also be a particularly bad place for a supply chain compromise to occur.
Adding a cooldown to even a single repo taught me a few things.
Add a required-version, e.g. required-version = ">=0.11.33"
required-version helps ensure developers are using a new enough version locally.
uv introduced relative cooldowns (e.g. “7 days”) in 0.9.17 so consider that the absolute floor. Timestamp-based cooldowns were added much earlier, but these are too clunky.
I originally targeted >=0.10.0 to incorporate some relevant bug fixes that landed in later versions of 0.9.x. Check the release notes for your package manager to determine the appropriate version.
When to Add Cooldown Exclusions
Exclusions are natively supported by uv and provide a clean way to exempt certain packages from cooldowns.
In my opinion, allowing devs to manage their own exclusions is fine. If a developer wants to use a package version that came out yesterday because it fixes a known vuln or adds useful functionality, that’s fine. Cooldowns are about setting a secure default behavior, not rigid governance.
Not all registries provide upload timestamps
If the registry doesn’t tell uv when a package was uploaded, you’ll need to add an exclusion.
buf.build, which we use for protobufs, originally didn’t provide timestamps, so I had to add exclusions for packages from that registry. They now support timestamps.
Internal packages
An internal package is unlikely to include malware and requiring people to wait a week for their changes to be available would be a frustrating developer experience.
Recently installed packages
There was also one package that was installed less than a week ago. Instead of downgrading this to an earlier version, or giving it a permanent exception, I gave it a time-based exception.
This reduced the cooldown period for this package only, while still protecting you from a newly released malicious version.
You can see an example that illustrates all of this below:
# pyproject.toml - on 2026-08-26
[tool.uv]
required-version = ">=0.11.33" # uv version requirement
exclude-newer = "1 week" # 1 week cooldown
[tool.uv.exclude-newer-package]
bufbuild-protovalidate-protocolbuffers-python = false # no upload timestamps
semgrep-app-client = false # internal package
pydantic-ai-harness = "2026-08-24T00:00:00Z" # recently installed
After successfully merging PRs to a couple repos, I was ready to scale this out.
My initial rollout
How to Message Your Engineering Team
Start by giving your eng team a heads up about what you’re doing and what they need to do to keep doing their job. For cooldowns I’d keep it brief:
What is a cooldown and why is it useful
Which package managers used by your org support cooldowns
Minimum package manager version and how to update, e.g. uv self update
How and when to add exclusions
The more stuff you add the more likely people are to ignore you, so keep it succinct.
Prior to sending out comms, it’s good to figure out versions of your package manager that pre-date cooldowns handle them. Are they ignored or do they cause errors? Is the error helpful?
Creating an Inventory of Repos Without Cooldowns
I wasn’t sure how much work it would be to roll this out org-wide, so I started by writing Semgrep rules to find uv and yarn configs without cooldowns. A security engineer and Semgrep power user at Squarespace published a bunch of publicly-available rules that you can use to detect missing cooldowns across a variety of package managers.
Which Repos to Start With
Start with a couple of repos you feel comfortable working in, but your goal should be to onboard repos under active development, especially those with a lot of developers.
Automating Cooldowns with a Claude Command
My first attempt at Claudomating rollout of cooldowns was to create a Claude Command, /uv-cooldown. You can see the prompt here, but it would handle:
Skipping pyproject.toml files managed by other Python package managers, e.g. Poetry
Setting the cooldown, cooldown exemptions, and the uv minimum version in the pyproject.toml file
Updating uv if it was below the minimum version across the project, including Dockerfiles, CI jobs, and one off installs like pre-commit
Opening PRs
The biggest problem was that I was driving too much of the process. I was cloning repos and running the slash command. This wasn’t too bad because I could open a dozen Claudes and tab through them answering questions/monitoring progress. After a couple of batches I had covered most of the repos I cared about. This wasn’t too time consuming because Semgrep doesn’t have a ton of repos.
Migrating from poetry to uv
When I did this initial phase of work, Poetry (another Python package manager) did not support cooldowns. That has since changed, but for unrelated reasons we were already migrating from poetry to uv across Semgrep. I made a second command for migrating a repo and then adding a cooldown.
We didn’t have too many Poetry repos to migrate, and LLMs are pretty good at this kind of thing so I figured I might as well do it instead of trying to convince an eng team to do it for me.
Things went pretty smoothly, although I did break one service for a few minutes 😬
We have some services that run read-only k8s containers. When migrating from poetry to uv Claude didn’t set the entry point to the already built venv and instead reinstalled the deps during startup. I didn’t catch this change, and CI passed so I assumed everything was fine. uv couldn’t install the deps and so the service crashed because the deps it needed weren’t where it was expecting them to be.
Luckily this was caught quickly, and I updated the command to call the installation folder created at build time. In addition to not crashing, this also improved the start-up time since it didn’t reinstall the dependencies from the build step. I also added a test to catch this problem in CI, hopefully preventing someone from running into the same problem in the future.
Scaling Cooldowns Across Every Repo
The first phase covered most of our Python repos under active development. I got busy with some other projects and picked this work back up a couple months later. The next section handles the second version of my rollout and my additional learnings.
Semgrep Agentic Workflows
Semgrep Agentic Workflows are written in Python, allow you to combine deterministic tooling and LLMs, and orchestrate runs across your repos. If you read my GitHub Actions SHA pinning blog from a couple weeks ago, you’ll know that I’m Workflows-pilled for making org-wide changes due to how easy it is to go from “this works locally” to “this runs across all repos.”
For the long tail of repos, I wanted to use Workflows so that I could avoid cloning a bunch of repos and running /uv-cooldown. If you’re a Semgrep customer and want to use my Workflow, reach out to your Semgrep contact.
If you’re not a Semgrep customer you could create a GitHub App, give it access to your repos, clone the code for each repo, and recreate my steps using the guide below.
Migrating from a Claude Command to Workflows
The Workflow still uses Claude under the hood and mostly follows the same flow as the older Claude Command, but I did make some improvements as part of the migration. Here’s the summary of what the Workflow does:
Glob for pyproject.toml and uv.lock, skip projects managed by Poetry, etc.
Consult the uv docs and release notes. This was my attempt to supplement the agent’s outdated training data. It seemed like it helped, but tbd on how it performs long term.
Add the cooldown
Review the manifest and lockfile for internal packages and problematic registries. Intelligently apply registry-level and package-level exemptions.
a. index-exclude-newer is a uv preview feature that allows you to exempt a package index from cooldowns. It didn’t exist when I did the original phase of work. I think this feature is great, but consider whether a preview feature is right for your org.
b. We host our internal packages on CodeArtifact so I added an exemption for that index and deleted the individual package exemptions I had added previously. This also avoids someone in the future needing to add a package-level exemption for their newly-created internal package. Think twice about using this if your private registry also mirrors public packages.
Use uv lock to regenerate the lockfile. In theory, this should just add the cooldown. If the previous step correctly added the exemptions, nothing should get downgraded.
a. The original Claude Command added a cooldown, regenerated the lockfile, looked for downgrades, added package-level exemptions for downgraded packages, then regenerated the lockfile a second time and compared the diffs. This is a bit cleaner.
Raise the minimum uv version across required-version, Dockerfiles, CI, pre-commit, etc. If a repo is already above 0.11.33, the uv version is left untouched.
a. Previously, I set the floor to 0.10.0 but updated it to 0.11.33 for this pass. Registry-level exclusions were added in 0.11.5 and there were exclude-newer related bug fixes throughout 0.11.x, including 0.11.33.
b. In my experience uv has done a good job maintaining backwards compatibility. If your org is more sensitive to package manager version changes you could consider something more sophisticated when picking the floor, e.g. you could set the floor to 0.9.17 and only use registry-level exemptions if the repo is already above 0.11.5.
c. UV_EXCLUDE_NEWER can be used to set a cooldown for installs that aren’t driven by a project manifest, e.g. UV_EXCLUDE_NEWER="1 week" uv tool install pre-commit --with pre-commit-uv
Claude checks its work by running uv lock --check and entering a bounded repair loop, where it has 5 chances to fix any issues.
Assuming verification passes, open a PR with a concise body
Poetry added cooldown support in version 2.4. I wrote a similar workflow that adds cooldowns and upgrades Poetry when the installed version is below the 2.4 minimum. In my somewhat limited testing, it also handled the nuances of migrating from Poetry 1.x to 2.x.
With the conversion to a Semgrep Agentic Workflow complete I was able to easily bulk update repos to add cooldowns. Workflows can also be run on a schedule or in response to completed scans. For example, you could run the cooldown Workflow when a new repo without cooldowns gets scanned.
I also added cooldown guidance to our org-wide CLAUDE.md file, which has helped ensure cooldowns are consistently applied going forward.
Archiving Unused Repos
Earlier this year I archived a couple hundred repos with zero contributions in the last year that nobody at Semgrep spoke up to keep. Archiving old junk is great for projects like this; every repo you archive is one less repo for you to add a cooldown. And you can always unarchive it later if someone needs it.
Fix Broken CI as you go
Whenever you do projects like this you inevitably end up making PRs to a bunch of semi-abandoned repos that have broken CI. If it is an easy fix, have Claude include it in the PR. PRs with green checks are easier to get merged and it helps build some goodwill with developers.
Summary
In the last year TeamPCP has compromised 1,000s of packages, and they aren’t the only ones running this playbook. Cooldowns are a great way to avoid installing malicious package versions, and using the native package manager implementations provides a good developer experience.
Rollout tips:
Add cooldowns to a few familiar repos to learn the basics. Read release notes and docs for relevant package managers.
Create org-appropriate messaging. Why are they important, minimum required package manager version and how to update, and when to add exceptions. Monitor Slack channels where people would ask for help.
Add cooldown guidance to your org-wide CLAUDE.md (or similar)
Create an inventory of repos with and without cooldowns to track your progress. You can do this for free using Semgrep Community Edition + these rules. Archiving unused repos helps cut down your to-do list.
Use my Workflows or write your own functionality to handle adding cooldowns and upgrading your package managers. This section is helpful if you DIY.
Cooldowns provide a secure default; they aren’t meant to be a rigid control. They have built-in exceptions for a reason. Use these for internal packages or when you need the latest features or bug fixes.