The last two weeks of March 2026 were brutal for software supply chains. A cascading campaign by a threat actor tracked as TeamPCP started with a compromised credential in Trivy (Aqua Security’s vulnerability scanner), which was then used to hijack Checkmarx KICS and then LiteLLM (an LLM proxy with ~97M monthly PyPI downloads). On top of that, axios – the most popular HTTP client on npm with ~100M weekly downloads – was compromised through a stolen maintainer token, with Google’s Threat Intelligence Group attributing the attack to North Korea’s UNC1069.

In both cases, the malicious versions were live on the registry for only 2-3 hours before being pulled. That’s a tiny window – but if your CI happened to run npm install or pip install during those hours, you’d have pulled a RAT dropper into your build.

These attacks are genuinely pretty scary. One simple defense tactic could be summarized as do not install a package version until it has been on the registry at least for some amount of time.

William Woodruff’s analysis found that of 10 recent supply chain attacks examined, 8 had exploitation windows under one week. A 7-day cooldown would have blocked most of them. Andrew Nesbitt’s “Package Managers Need to Cool Down” provides the most comprehensive survey of the landscape.

The good news is that nearly every major package manager now supports this natively.

Python#

uv (since v0.9.17, Dec 2025) has the most ergonomic implementation – it accepts relative durations like "7 days", absolute timestamps, and even per-package overrides via exclude-newer-package:

toml
# ~/.config/uv/uv.toml
exclude-newer = "7 days"

pip (since v26.0, Jan 2026) supports cooldowns via uploaded-prior-to in pip.conf, but only accepts absolute timestamps. You have to keep updating the value manually, or use Seth Larson’s crontab workaround to automate it:

ini
# pip.conf  (under [global] or [install])
uploaded-prior-to = 2026-03-24T00:00:00Z

Poetry and Conda have open proposals but nothing shipped yet.

JavaScript#

All five major JS package managers now support this, though they each picked a different unit – a running joke in the HN thread.

npm (since v11.10.0, Feb 2026) – in days:

ini
# ~/.npmrc
min-release-age=7
ignore-scripts=true

The ignore-scripts=true is worth adding too – the axios attack relied on a postinstall script to execute the RAT dropper, and this would have independently blocked it regardless of cooldown.

pnpm (since v10.16, Sep 2025) – in minutes, with an exclude list for packages you trust:

ini
# ~/Library/Preferences/pnpm/rc  (or ~/.npmrc for pnpm)
minimum-release-age=10080

Bun (since v1.3, Oct 2025) – in seconds, with an exclude list:

toml
# ~/.bunfig.toml
[install]
minimumReleaseAge = 604800

Yarn (since v4.10.0, Sep 2025) – also minutes, but with a completely different setting name:

yaml
# .yarnrc.yml
npmMinimalAgeGate: 10080

Deno (since v2.6) – human-readable durations:

jsonc
// deno.json
{
  "install": {
    "minimumDependencyAge": "7d"
  }
}

Dependency update bots#

If you use Renovate or Dependabot to automate dependency updates, they have their own cooldown settings that work across ecosystems.

Renovate has long supported minimumReleaseAge with human-readable durations. Mend’s security:minimumReleaseAgeNpm preset enforces a 3-day minimum for npm by default:

json
// renovate.json
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "minimumReleaseAge": "7 days"
}

Dependabot (GA since July 2025) has a cooldown block with semver-level overrides – security updates bypass cooldown entirely:

yaml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "daily"
    cooldown:
      default-days: 7
      semver-major-days: 30
      semver-minor-days: 7
      semver-patch-days: 3

Snyk applies a non-configurable 21-day cooldown by default.

Other ecosystems#

  • Ruby: gem.coop implements a registry-level 48-hour delay on newly published gems – no client-side changes needed.
  • Rust/Cargo: crates.io now publishes created_at timestamps in the index (v1.94). There’s no built-in config-file cooldown yet, but the third-party cargo-cooldown wrapper enforces one. Cargo’s approach leans toward explicit opt-in via cargo update --precise.
  • Go, Composer, NuGet: Open proposals (Go #76485, Composer #12633, NuGet #14657) but nothing shipped.

What cooldowns don’t cover#

The Trivy attack worked differently – it force-pushed Git tags to redirect trusted references to malicious commits, exploiting Git’s mutable tag system. Package manager cooldowns wouldn’t have helped there. The defense for that vector is pinning GitHub Actions to full commit SHAs rather than mutable tags:

yaml
# Bad: tag can be force-pushed
- uses: aquasecurity/trivy-action@v0.69.4

# Good: immutable reference
- uses: aquasecurity/trivy-action@a3e1b43e...

Cooldowns also won’t help against a patient attacker who publishes a clean version, waits out the cooldown, and then inserts malicious code in a later update. But the counter-argument is compelling: security scanners like Socket and StepSecurity are actively monitoring new publishes and tend to flag malicious packages within hours. The cooldown buys time for that “herd immunity” to kick in.

Further reading#