Cooldown¶

mpm can refuse to install or upgrade any package version younger than a chosen release age. This is a supply-chain safeguard: malicious releases (compromised credentials, dependency confusion, account takeover) are typically detected and pulled from registries within days of publication, so a short waiting period keeps the most recent and most likely compromised versions off the system.

Recent examples include the XZ Utils backdoor and recurring vulnerabilities in the VS Code extension marketplace. A delay of even a few days would have given the community time to react.

Quick start¶

The cooldown applies to every install and upgrade mpm performs:

$ mpm --cooldown "7 days" upgrade --all
$ mpm --cooldown "1 week" install some-package
$ mpm --cooldown best-effort upgrade --all   # posture only: the window comes from the configuration
$ mpm --cooldown off upgrade --all           # disable the gate for this run

If you are unsure which value to pick, --cooldown '7 days' is a reasonable default: long enough for most compromised releases to be flagged and pulled from registries, short enough to keep security updates timely. There is no bare --cooldown shorthand: the option always takes an explicit value, a duration or a keyword.

--cooldown accepts four input shapes:

  • Friendly duration: 7 days, 1 week, 12h, 30m, 45s, a bare number of days (7), or 0 to disable the gate for this run.

  • ISO 8601 duration: P7D, PT12H, P1WT6H. Case-insensitive.

  • RFC 3339 absolute timestamp: 2024-05-01T00:00:00Z or with an offset like +02:00. Converted at parse time to now - timestamp; a timestamp in the future disables the gate.

  • Enforcement keyword: enforce, best-effort or off, selecting the posture for managers that cannot natively enforce the window (skip them, run them without the safeguard, or disable the gate entirely). A keyword carries no window of its own: it comes from the configuration, which is what makes the keywords a one-run override rather than a full setting.

A duration sets the window for this run and leaves the posture to the configuration; a keyword sets the posture and leaves the window to the configuration; off and 0 are two spellings of the same thing. The window is settable as the period key of the [mpm.cooldown] table in any mpm configuration file, with the policy key carrying the posture (see Configuration for the full schema), or a one-run window as the MPM_COOLDOWN environment variable.

Note

Durations resolve to a fixed number of seconds, assuming a day is 24 hours. The local time zone, DST transitions, and calendar boundaries are ignored. Calendar units (months, years) are rejected for the same reason: 28-31 days and 365-366 days make them unsuitable for a precise release-age cutoff. Use days or weeks instead.

How it works¶

When cooldown is set, mpm:

  1. Computes a UTC cutoff timestamp equal to now - cooldown.

  2. For each manager that natively enforces a release-age gate (see the support table below), injects the manager’s dedicated environment variable carrying that cutoff into every CLI call. The manager’s own resolver then excludes every version published after the cutoff, including transitive dependencies.

  3. For each manager mpm gates with its own per-package probe (flatpak, mas and the AUR half of paru today), reads the publication date of each package’s latest release through the manager’s own CLI and holds back the too-fresh ones individually, while the rest of the operation proceeds. A package whose date cannot be read is held too (fail-closed). An upgrade --all on such a manager either rides the manager’s native exclusion flag (paru’s --ignore keeps the one --sysupgrade transaction) or runs package by package (flatpak, mas), since only those two shapes can hold back part of the set.

  4. For each manager with neither mechanism, skips install / upgrade with a warning (fail-closed). Re-run with --cooldown best-effort (or set policy = "best-effort" in the [mpm.cooldown] table of the config file) to run those managers anyway, without the safeguard. The posture can also be pinned for a single manager with a cooldown_policy override (best-effort runs it unguarded, off exempts it from the gate entirely), see per-manager overrides.

  5. Leaves read-only operations (outdated, installed, search) untouched: information is never blocked, only mutations are.

The choice to delegate to each manager’s own resolver rather than reimplement the gate inside mpm is deliberate: only the resolver can apply the cutoff to the whole dependency closure (see Limitations below). The per-package probe is the measured exception, reserved for managers whose install unit is a self-contained artifact with no dependency resolution at install time: holding back the named package is then the whole gate, and the probe only ever reads server-set publication dates a compromised author cannot backdate.

Supported managers¶

The table below is the source of truth for which managers mpm can gate today and the state of the upstream effort everywhere else. Statuses:

  • âś… Enforced: mpm actively gates the manager, injecting a cooldown environment variable on every CLI call (the cooldown_env_var framework) or holding back each too-fresh package through its own per-package release-date probe.

  • 🔜 Shipped upstream: the manager ships a release-age gate but mpm does not (yet) plug into it.

  • đźš§ Proposed: an open pull request, RFC, or issue is on file upstream.

  • ❌ None: no public proposal found, or the gate that shipped upstream never reaches the commands mpm drives.

  • âž– N/A: the concept does not apply (distro-curated repositories with their own staging, archived projects, meta-upgraders, …). A structural equivalent is noted when relevant.

mpm id

Status

Mechanism

Reference

am

❌ None

—

—

antidote

🔜 Shipped upstream (since 2.2.0)

zstyle ':antidote:bundle:*' min-age <days>, whole days, no environment variable

antidote CHANGELOG

antigen

❌ None (clones a Git branch or tag, no release feed)

—

—

apk

❌ None

—

—

apm

âž– N/A (archived June 2022)

—

atom/apm

apt

➖ N/A (Debian’s unstable → testing → stable migration is functionally similar)

—

Nesbitt, Package managers need to cool down

apt-cyg

➖ N/A (unmaintained since 2017; wraps Cygwin’s setup.exe repository)

—

transcode-open/apt-cyg

apt-mint

âž– N/A (follows apt)

—

—

aptitude

âž– N/A (same Debian archives as apt)

—

—

asdf

❌ None (runtime version manager)

—

—

aura

❌ None (Arch AUR helper; could take paru’s probe)

—

—

bin

❌ None

—

—

bob

❌ None (installs Neovim’s own GitHub releases)

—

—

basalt

❌ None

—

—

bpkg

❌ None

—

—

brew

đźš§ Proposed (closed as not planned for users; merged for internal bottle resource resolution)

(internal) --min-release-age=1, --uploaded-prior-to

Homebrew/brew#21129

bun

❌ None

—

—

cargo

❌ None in the cargo install scope (the stabilized gate covers dependency resolution only)

CARGO_REGISTRY_GLOBAL_MIN_PUBLISH_AGE, ignored by cargo install

rust-lang/cargo#17335

cask

đźš§ Same as brew (inherits)

—

Homebrew/brew#21129

cave

❌ None (Exherbo, source-based)

—

—

choco

❌ None

—

—

choosenim

❌ None (the catalog is a git tag list, which carries no publication date)

—

—

chromebrew

❌ None (Chrome OS)

—

—

claude-code-plugins

❌ None (marketplace manifests carry no publication date)

—

—

composer

đźš§ Proposed

open PR adds cooldown

composer/composer#12692

conda

đźš§ Proposed

--exclude-newer / exclude_newer (open issue + PR)

conda/conda#15759

clib

❌ None

—

—

cpan

❌ None

—

—

deb-get

❌ None

—

—

dkp-pacman

❌ None (pacman fork over devkitPro’s own repositories)

—

—

dnf

❌ None (effort focused on dnf5)

—

—

dnf5

đźš§ Proposed

minimum_package_age (open issue)

rpm-software-management/dnf5#2743

dotnet

đźš§ Proposed (scoped to dotnet package flows, not dotnet tool)

minPublishAgeHours per source in nuget.config (open spec)

NuGet/Home#14657

elan

❌ None (installs Lean’s own GitHub releases)

—

—

emacs

❌ None (ELPA archives publish versions, never dates)

—

—

emerge

❌ None

—

—

eopkg

❌ None

—

—

fink

❌ None (macOS, Debian/dpkg-based)

—

—

fisher

❌ None (clones a Git branch or tag, no release feed)

—

—

flatpak

âś… Enforced (mpm per-app probe)

ostree commit date read by flatpak remote-info, each too-fresh app held back

flatpak remote-info

fwupd

âž– N/A (LVFS staged deployment)

—

LVFS news

gcloud

❌ None

—

—

gem

❌ None in the gem scope (shipped for Bundler, which mpm does not drive)

bundle config cooldown / BUNDLE_COOLDOWN, Bundler only

ruby/rubygems#9576

getnf

❌ None

—

—

gh-ext

❌ None (installs extensions from git repos)

—

—

gext

❌ None

—

—

ghcup

❌ None

—

—

go

❌ None (Go module proxy, no publication-date filter)

—

—

guix

❌ None

—

—

gup

❌ None (Go module proxy, no publication-date filter)

—

—

haxelib

❌ None

—

—

hyprpm

❌ None (a plugin repository is a git clone, which carries no publication date)

—

—

ips

❌ None (packaging dates are publisher-set manifest metadata)

—

—

jpm

❌ None

—

—

julia

❌ None (registry entries carry versions and hashes, never dates)

—

—

juliaup

❌ None

—

—

krew

❌ None

—

—

lazy

❌ None

—

—

luarocks

❌ None

—

—

lure

❌ None (a package is a build recipe in a git repository, carrying no publication date)

—

—

macports

❌ None

—

—

mamba

❌ None on the CLI (libmambapy API shipped in mamba 2.9.0)

exclude_newer_timestamp, libmambapy only

mamba-org/mamba#4228

mason

❌ None

—

—

micromamba

❌ Same as mamba (inherits)

—

—

mas

âś… Enforced (mpm per-app probe)

App Store currentVersionReleaseDate read by mas lookup --json, each too-fresh app held

iTunes Search API

micro

❌ None

—

—

miktex

❌ None

—

—

mise

🔜 Shipped upstream

minimum_release_age env MISE_MINIMUM_RELEASE_AGE / --minimum-release-age (default 24h)

mise settings

nala

❌ None

—

—

nimble

❌ None

—

—

nix

❌ None

—

—

npm

✅ Enforced (npm ≥ 11.10)

min-release-age env npm_config_min-release-age (integer days)

npm docs

oh-my-fish

❌ None (clones a Git branch or tag, no release feed)

—

—

ollama

❌ None

—

—

opam

❌ None

—

—

opkg

❌ None

—

—

pacaur

❌ None (Arch AUR helper)

—

—

pacman

❌ None

—

—

pacstall

❌ None

—

—

pamac

❌ None

—

—

pear

❌ None

—

—

paru

âś… Enforced (mpm per-package probe, AUR half)

AUR LastModified read by paru --sync --info; held packages ride --ignore

AUR RPC

pearl

❌ None (a package is a git checkout, carrying no version or date)

—

—

pi

❌ None (a source spec names no version, let alone a date)

—

—

pikaur

❌ None (Arch AUR helper; could take paru’s probe)

—

—

pip

✅ Enforced (pip ≥ 26.1)

--uploaded-prior-to env PIP_UPLOADED_PRIOR_TO

pypa/pip#13674

pipx

✅ Enforced (via pip’s env var; needs the underlying pip ≥ 26.1)

inherits PIP_UPLOADED_PRIOR_TO

pypa/pipx#1811

pixi

đźš§ Proposed (shipped for workspaces since 0.47.0, never for pixi global)

exclude-newer in pixi.toml, declarative only: no flag, no environment variable

prefix-dev/pixi#5810

pipxu

❌ None (no release-age knob; uv gains one only through its own env)

—

—

pkcon

âž– N/A (delegates to the distro backend)

—

—

pkg

❌ None

—

—

pkg-tools

❌ None (OpenBSD, per-release curated)

—

—

pkgit

❌ None (a package is a git checkout, carrying no version or date)

—

—

pkgin

❌ None (pkgsrc binary packages)

—

—

pkgm

❌ None (pkgx pantry, no publication date on any surface)

—

—

platformio-core

❌ None (registry dates a release, but no declared operation reads them)

—

—

pnpm

✅ Enforced (pnpm ≥ 11.0)

minimumReleaseAge env pnpm_config_minimum_release_age (minutes)

pnpm docs

ports

❌ None (FreeBSD ports)

—

—

prt-get

❌ None (CRUX ports)

—

—

pwsh-gallery

❌ None

—

—

pyenv

❌ None

—

—

raco

❌ None

—

—

roswell

❌ None (installs Lisp implementations from GitHub releases)

—

—

rustup

❌ None

—

—

scoop

đźš§ Proposed

open feature request

ScoopInstaller/Scoop#6513

sdkman

❌ None

—

—

sfsu

đźš§ Inherits from scoop

—

—

sheldon

❌ None (clones a Git branch or tag, no release feed)

—

—

shelly

❌ None (Arch ALPM front-end, like pacman)

—

—

skills

❌ None (skills carry no version, let alone a publication date)

—

—

slapt-get

❌ None (Slackware)

—

—

snap

âž– N/A (risk channels stable/candidate/beta/edge, plus snap refresh --hold up to 90 days)

snap refresh --hold

Snap docs

soar

❌ None (static-binary installer)

—

—

sorcery

❌ None (Source Mage, source-based)

—

—

spack

❌ None (builds from source)

—

—

steamcmd

❌ None

—

—

stew

❌ None

—

—

sun-tools

âž– N/A (legacy SVR4 tooling superseded by IPS)

—

Oracle: SVR4/IPS comparison

swupd

âž– N/A (Clear Linux discontinued July 2025)

—

Clear Linux OS

tazpkg

❌ None (SliTaz)

—

—

tlmgr

❌ None (TeX Live, curated)

—

—

topgrade

âž– N/A (meta-upgrader; delegates to each underlying manager)

—

—

trizen

❌ None (Arch AUR helper; could take paru’s probe)

—

—

urpmi

❌ None (Mageia)

—

—

uv, uvx

âś… Enforced

exclude-newer env UV_EXCLUDE_NEWER

uv docs

vcpkg

❌ None

—

—

vim-pack

❌ None

—

—

volta

âž– N/A (unmaintained since November 2025)

—

volta-cli/volta

vagrant

❌ None

—

—

vscode, vscodium

đźš§ Proposed for installs (an auto-update delay shipped in 1.125.0)

extensions.autoUpdateDelay, auto-updates only

microsoft/vscode#321136

winget

đźš§ Proposed

open feature request

microsoft/winget-cli#6178

xbps

❌ None

—

—

yazi

❌ None

—

—

xcodes

❌ None

—

—

yarn (Classic v1)

❌ None (project in maintenance mode)

—

yarnpkg/yarn

yarn-berry

➖ N/A (Yarn Berry removed global installs, so mpm implements no install / upgrade / remove to gate; the upstream gate, Berry ≥ 4.10, covers project installs)

npmMinimalAgeGate

Yarn settings

yay

✅ Enforced (yay ≥ 13.0)

generated init.lua overlay via XDG_CONFIG_HOME (UpgradeSelect + AURPreInstall hooks)

Jguer/yay#2883

yum

âž– N/A (deprecated alias for dnf on RHEL-family)

—

—

zef

❌ None

—

—

zerobrew

❌ None

—

—

zeroinstall

❌ None (dates are author-written in a publisher-hosted feed)

—

—

zim

❌ None (clones a Git branch or tag, no release feed)

—

—

zinit

❌ None

—

—

zplug

❌ None (clones a Git branch or tag, no release feed)

—

—

zvm

❌ None (the catalog lists versions, never their release dates)

—

—

zypper

❌ None

—

—

Notes¶

  • brew ships internal release-age gates inside Homebrew’s bottle resource-resolution pipeline so formulae built from upstream resources get a delay automatically. Coverage expanded with Homebrew 6.0.0 to include npm and pip (Homebrew/brew#21919), PyPI (Homebrew/brew#21920), RubyGems (Homebrew/brew#22253), and Bundler (Homebrew/brew#22555). All four gate source dependencies fetched during a formula build; none expose a user-facing knob. The issue requesting one for brew install (Homebrew/brew#21129) was closed as not planned. The scope is distinct from mpm’s --cooldown, which gates the package version mpm asks brew to install: brew’s internal gate keeps build-time dependencies fresh-but-not-too-fresh, mpm’s gate delays the package version itself. Both can be in effect simultaneously without conflict.

  • cargo stabilizes min-publish-age for Rust 1.100.0 (rust-lang/cargo#17335). One environment variable would drive it, CARGO_REGISTRY_GLOBAL_MIN_PUBLISH_AGE, because resolver.incompatible-publish-age already defaults to deny. The gate covers dependency resolution alone. cargo install ignores it both for the crate it installs and for that crate’s dependencies, which upstream pins with its cargo_install_ignores_min_publish_age and cargo_install_allows_too_new_deps tests. mpm drives cargo install, so nothing it runs is gated until that scope widens.

  • pipx delegates to whichever pip lives inside its managed virtualenvs. If that pip predates 26.1 (or pipx routes resolution through uv instead), PIP_UPLOADED_PRIOR_TO is silently ignored. mpm has no clean way to inspect pipx’s internal resolver, so treat the pipx gate as best-effort.

  • pixi ships a real release-age gate, exclude-newer, but only for workspaces: pixi global hard-codes exclude_newer: None at every solve site, and that is the scope mpm drives. The setting is also declarative-only, with no flag and no environment variable to inject (prefix-dev/pixi#5018), so even a global-scope gate would need a knob before mpm could reach it. Hence đźš§ rather than 🔜: what is missing upstream is the part mpm would use.

  • dotnet: NuGet’s cooldown design (NuGet/Home#14657) is under implementation, but its V1 enumerates dotnet package gestures and the Visual Studio surfaces only. dotnet tool install and dotnet tool update, the commands mpm drives, appear in no tracking issue. The setting is per-source in nuget.config, which the tool commands do read, so the scope may widen later.

  • gem: the cooldown setting merged into Bundler (ruby/rubygems#9576), which reads it from bundle config, BUNDLE_COOLDOWN or a per-source cooldown: value. It gates the bundle commands. mpm drives the gem CLI instead, where no equivalent is on file.

  • mamba and micromamba: mamba 2.9.0 shipped the cutoff as a libmambapy setting, exclude_newer_timestamp (mamba-org/mamba#4228). It serves conda’s own --exclude-newer work (conda/conda#15759). Neither command line exposes a flag or an environment variable of its own.

  • vscode and vscodium: VS Code 1.125.0 added extensions.autoUpdateDelay, an hour count holding back automatic extension updates, with a matching enterprise policy. It covers automatic updates only. mpm installs and upgrades extensions with code --install-extension, which the delay never reaches (microsoft/vscode#321136).

  • yarn-berry: the gate works in Berry ≥ 4.10, but Yarn Berry removed yarn global, so mpm’s yarn-berry handler only implements search. Onboarding npmMinimalAgeGate would not change anything reachable through mpm.

  • apt, snap, fwupd all have structural delays (Debian’s migration windows, Snap risk channels, LVFS staged deployment) rather than per-version age gates. They’re marked N/A because the underlying ecosystem solves the problem in a different shape.

  • flatpak is gated by mpm’s own per-app probe: before each install or upgrade, flatpak remote-info reports the ostree commit date of the app’s latest build, stamped by the remote’s build service (Flathub’s buildbot for the flathub remote) rather than by the app’s author, so a compromised publisher cannot backdate it. mpm covers flatpak applications only, and under an active cooldown upgrade --all switches from one flatpak update to one update per eligible app: a too-fresh app is held back individually, and runtimes follow the apps that need them instead of updating on their own. The moment between the probe and the update is not pinned, so a build published in that exact window can slip through; flatpak update --commit= could close it later.

  • mas runs the same per-app probe against the Mac App Store: mas lookup --json relays the catalog’s currentVersionReleaseDate, stamped by the store when a version goes live after review, so the app’s author cannot backdate it. App Review already delays every release on its own, which makes the App Store one of the more staged registries the gate covers: the window mostly buys time between a version going live and a takedown. The probe-to-install moment is not pinned either, the store serving only its current version.

  • yay added user-programmable Lua hooks in v13. An UpgradeSelect hook can filter the upgrade set by AUR LastModified age (yay ships an example at doc/examples/recently_modified.lua), and the maintainer closed the dedicated minimum-release-age requests (Jguer/yay#2824, Jguer/yay#2848) in favor of it. mpm now drives that machinery for a per-run cooldown by overlaying a generated init.lua through a private XDG_CONFIG_HOME: the overlay chains the user’s real init.lua (so nothing in their config is lost) and registers two hooks keyed off the same now - cooldown cutoff (passed as MPM_COOLDOWN_EPOCH). UpgradeSelect holds back AUR upgrades younger than the floor on yay -Syu, and AURPreInstall aborts a fresh yay -S whose AUR base is younger than the floor, closing the gap UpgradeSelect alone left open (Jguer/yay#2883). This needs yay ≥ 13; an older yay stays a usable manager but cannot honor the cooldown.

  • paru carries mpm’s per-package probe on its AUR half: paru --sync --info reports the AUR RPC’s server-set LastModified, the same clock the yay overlay gates on, and a too-fresh AUR package rides --ignore out of the single --sysupgrade transaction, so dependency ordering stays paru’s job. Official-repository packages pass untouched: Arch’s archive stages its releases on its own, which is the same reasoning that files the distro managers as N/A. The probe forces LC_ALL and TZ so paru’s localized, local-time rendering parses deterministically.

  • The other Arch AUR helpers ship no cooldown setting of their own and inherit whatever delay the AUR provides, which is none. pacaur is left ungated because its upstream is unmaintained; pikaur and trizen could take paru’s probe, each needing its own verified --sync --info parsing.

  • choco and pwsh-gallery were assessed for the per-package probe and deliberately left out. Both sit on NuGet-style feeds whose server-set Published timestamp is even reachable from their CLIs: choco info renders it (in the OS locale’s short date format, ambiguous to parse back), and PSResourceGet’s Find-PSResource exposes a PublishedDate property. What disqualifies them is the install side: choco install and Install-PSResource both resolve NuGet-style dependencies at install time, so holding the named package would leave its freshly-published dependencies ungated, the exact trap of the transitive-dependency gap. Their route is a native gate upstream, like the other resolver ecosystems.

Retraction paths by registry¶

A cooldown assumes something happens during the wait. If a compromised release can never be withdrawn, holding it for seven days only delays installing it by seven days: the version ages into eligibility unchanged. The safeguard’s value therefore rests on the registry behind each manager having a retraction path, and on that path being reachable by the resolver rather than merely advisory.

Retraction is a property of the registry, not of the manager CLI. pip, pipx, uv and uvx all resolve against PyPI; npm, pnpm and yarn all against the npm registry. The table below is keyed by registry rather than by manager, using the purl type mpm already maps to it (the PURL_MAP of meta_package_manager.specifier).

Two independent axes are tracked:

  • Retraction decides whether waiting achieves anything at all.

  • Publish date decides whether mpm could emulate a gate where no native one exists: it is what the shipped per-package probes read, and what the advisory and block modes under Possible future directions are blocked on everywhere else.

They really are independent. GitHub release assets expose a trustworthy server-set publication date and have no retraction path whatsoever, so a cooldown there is easy to compute and worth very little.

Retraction shapes, strongest first:

  • Yank: the artifact stays downloadable for an exact pin, but the resolver stops selecting it. The shape a cooldown wants.

  • Unpublish: the version is deleted outright. Effective, at the cost of breaking pinned installs.

  • Relabel: the artifact is moved off the default channel or label, and stays reachable only by asking for it explicitly.

  • Index revert: nothing is retracted at the artifact level. The curated index or channel pointer moves back off the bad version.

  • Flag only: the version is marked but still resolves, so a cooldown gains nothing from it.

  • None: no registry-side path, short of the upstream author deleting their own release.

A blank cell means no citable public source was found. As in the table above, absence of evidence is not recorded as absence of the capability.

Registry (purl type)

mpm managers

Retraction

Publish date

PyPI (pkg:pypi)

pip, pipx, pipxu, uv, uvx

Yank (PEP 592): the file stays downloadable, but an installer “MUST ignore yanked releases, if the selection constraints can be satisfied with a non-yanked version”

âś… upload_time_iso_8601, plus a per-file yanked flag (JSON API)

npm registry (pkg:npm)

bun, npm, pnpm, yarn, yarn-berry

Unpublish within 72 h of publishing, then only with no dependents, under 300 weekly downloads and a single owner (policy). Past that, flag only: npm deprecate warns on install but does not stop resolution

âś… time, mapping each version to its publication timestamp (packument)

crates.io (pkg:cargo)

cargo

Yank (cargo yank): “does not delete any data”, and “Cargo will not use a yanked version for any new project or checkout without a pre-existing lockfile”

âś… created_at, plus yanked and yank_message (API)

RubyGems.org (pkg:gem)

gem

Unpublish: gem yank “permanently removes a gem you pushed to a server”, dropping it from the index

âś… created_at and built_at, plus a yanked boolean (API)

Packagist (pkg:composer)

composer

Index revert: versions are fetched “from tags you create in your VCS repository” (about), so deleting the upstream tag withdraws the version. The package-level abandoned field is flag only (API)

âś… per-version time and published-time (metadata)

PEAR (pkg:pear)

pear

âś… per-release <da> in the REST release document (example), which no pear command surfaces

NuGet.org (pkg:nuget)

dotnet

Yank (unlist): “nuget.org does not support permanent deletion of packages”, so an unlisted version leaves search and the gallery but “can still be downloaded and installed by using an exact version number” (policy). Malware is the carve-out, deleted outright, each removal landing in the public catalog as a PackageDelete leaf (catalog)

âś… published on each registration catalogEntry, but overwritten with 1900-01-01T00:00:00+00:00 the moment a version is unlisted, and absent from the search resource (API)

anaconda.org / conda-forge (pkg:conda)

conda, mamba, micromamba, pixi

Relabel: “we do not allow edits or the deletion of packages on conda-forge” (immutability); a bad artifact is labelled broken and “Users will no longer be able to install them by default” (procedure)

âś… upload_time, with the broken label carried in labels (API)

CPAN / PAUSE (pkg:cpan)

cpan

Unpublish: an author can delete a distribution from the mirrors, though “a secondary mirror network called BackPAN retains distributions even if they are deleted from CPAN” (CPAN)

AUR

aura, yay, paru, pacaur, pikaur, trizen

None at the version level: an AUR package is a git repository with no per-version artifact to withdraw, so remediation is a maintainer push or deletion of the whole package

✅ server-set LastModified, the push timestamp mpm’s yay overlay and paru probe gate on. Git commit dates are client-set (GIT_COMMITTER_DATE), forgeable, and never consulted

Homebrew taps

brew, cask

Index revert through the tap’s git history, over a package-level lifecycle: deprecate! still installs with a warning, disable! makes the action fail, removal deletes it (lifecycle)

❌ a formula records the upstream version, not its publication date

Distro binary archives (pkg:deb, pkg:rpm, pkg:alpm, pkg:apk)

apk, apt, apt-mint, aptitude, deb-get, dkp-pacman, dnf, dnf5, eopkg, ips, nala, opkg, pacman, pamac, pkg, pkg-tools, pkgin, shelly, slapt-get, tazpkg, urpmi, xbps, yum, zypper

Index revert: removal is an archive operation and the mirror is rebuilt without the package. Debian, for one, requires filing an RM: bug against ftp.debian.org (developers-reference)

❌ the version string is the distro maintainer’s build, carrying no upstream publication date

Haxe library registry (pkg:haxe)

haxelib

None client-side: the remoting API is submit-only, exposing processSubmit with no delete, unlisting or yank counterpart (SiteApi.hx), so withdrawing a release means Haxe Foundation intervention on the server

âś… server-set per-version release date, carried as VersionInfos.date (MetaData.hx)

Racket package catalog

raco

Index revert: pkgs.racket-lang.org maps a package name to a source checkout and a checksum rather than hosting the code, so withdrawal is the catalog entry being removed or repointed; the source repository it names can also vanish underneath it

❌ a package is pinned by the 40-character checksum of its source rather than by a release number, and the catalog publishes no upload date the client can read

Raku ecosystem (pkg:perl6)

zef

Immutable once published: fez, the ecosystem zef resolves against, accepts an upload per version and offers its authors no delete or yank verb, so a withdrawal is an administrative request rather than a client-visible operation

❌ the identity a distribution is known by carries a version, an authority and an API level but no date, and neither the listing nor the search reports one

Git repositories (bash)

basalt, bpkg

Upstream deletion: bpkg clones the owner/repo it is given straight from the forge, so there is no index standing between the name and the code and no retraction surface either; a package is withdrawn by its repository being deleted, renamed or made private

❌ nothing dates a release: the fetch resolves a branch or tag on the repository rather than a published artefact, so the only timestamp available is a commit date, which is client-set

Janet package listing

jpm

Index revert: the listing is a single curated pkgs.janet in janet-lang/pkgs mapping a symbol to a git URL, so withdrawal is a pull request dropping the entry; the code lives in third-party repositories that can vanish under it

❌ the listing pairs a name with a URL and nothing else, carrying no versions and so no dates; a package’s versions are the git tags of the repository it points at

clib package wiki

clib

Wiki edit: the index is a GitHub wiki page, clibs/clib/wiki/Packages, scraped as HTML (CLIB_WIKI_URL in src/clib-search.c), so withdrawal is an edit to that page; the code itself is fetched from the GitHub archive of whichever repository the row names, and disappears with it

❌ the index carries names, URLs and descriptions only, with no version field at all, so there is nothing to date a release by

Nim package index (pkg:nim)

nimble

Index revert: the registry is a single packages.json in nim-lang/packages mapping a name to a git URL, so withdrawal is a pull request deleting the entry; the code itself lives in third-party repositories that can also vanish under the index

❌ the index carries no versions at all, only names and URLs, versions being the git tags of the repository each entry points at, so nothing in it dates a release

LuaRocks (pkg:luarocks)

luarocks

Deletion, not retraction: luarocks.org is the single host and its site application exposes owner and administrator delete paths (modules.moon), with no yank or unlist concept, so a withdrawn version simply stops existing in the manifest

❌ the manifest the client downloads maps module to version to arch and carries no timestamp of any kind, so a release-age gate would have nothing client-side to filter on

Rust release channels (pkg:cargo toolchains)

rustup

None published: releases are static artifacts on the project’s own distribution host, with no yank, unlist or delete surface exposed to anyone outside the Rust release team; a withdrawal is a change to the channel manifest that host serves

âś… every channel manifest carries the release date of the toolchain it describes, which rustup check reports alongside the version

Haskell toolchain bindists

ghcup

None published: bindists are static artifacts named by ghcup’s own metadata file, which carries no yank or unlist surface; withdrawing a release means the Haskell.org release team editing the metadata that describes it

âś… the metadata records a release day per version, which ghcup list renders in its notes column

Julia release channels

juliaup

None published: channels resolve through a versions database juliaup distributes, which offers no yank or unlist entry; withdrawing a release means the Julia release team dropping it from that database

❌ the database names versions and their download artifacts, carrying no publication date for a release-age gate to read

Python interpreter source releases

pyenv

None, and no operator to ask: each version is a build recipe shipped inside pyenv naming an upstream tarball, so withdrawing one means either the implementation pulling its own release or a new pyenv dropping the recipe

❌ a recipe names a source archive and its checksum, carrying no publication date for a release-age gate to read

Apple developer downloads

xcodes

Vendor withdrawal only: releases are served from Apple’s developer downloads and catalogued by the community feed xcodes reads, so a version disappears when Apple stops serving it, with no yank or unlist surface exposed to anyone else

âś… the catalog feed records a release date per version, though mpm reads no release-age gate from it

Google Cloud SDK components

gcloud

Vendor withdrawal only: components are published solely by Google and described by the snapshot the CLI fetches, so a version leaves by being dropped from that snapshot, which is also how a still-installed component comes to report itself as deprecated

❌ the snapshot names versions and their archives, carrying no publication date for a release-age gate to read

Vagrant Cloud boxes

vagrant

Author deletion: a box version is published by its own author to the public registry and can be withdrawn there, the version disappearing from the metadata Vagrant reads while any copy already downloaded stays in the local box store

âś… the registry records a creation date per box version, though mpm reads no release-age gate from it

Ollama model registry

ollama

Author deletion: a model is published to the registry under its own namespace and withdrawn there, the tag ceasing to resolve while any copy already pulled stays in the local store

❌ the manifest carries a content digest and no publication date for a release-age gate to read

Git-manifest indexes

am, chromebrew, fink, guix, krew, vcpkg, micro, macports, nix, opam, scoop, sfsu, winget, zerobrew

Index revert: reverting the manifest, Portfile or derivation commit withdraws the version

❌ only the commit date, which is client-set and trivially backdated. A date declared in the manifest, like winget’s optional ReleaseDate, is author-supplied too and no sounder

Chocolatey Community Repository (pkg:chocolatey)

choco

Unlist: moderators can unlist individual package versions (list/unlist), and malicious scripts are grounds for banning the maintainer and deleting their packages (moderation)

PowerShell Gallery

pwsh-gallery

Yank under another name: the Gallery “does not support permanent deletion of packages”; an unlisted version leaves search and listings but “remains downloadable by specifying its exact version” (docs)

Snap Store

snap

Index revert on the channel pointer: “Promoting and demoting revisions is also a useful progression […] for when a snap needs to revert to an older version as a response to a security concern” (release management)

extensions.gnome.org

gext

Not documented: the review guidelines govern approval before publication and say nothing about withdrawing an extension afterwards, so what happens to a published extension later found malicious is unrecorded rather than known to be nothing.

Flathub

flatpak

Relabel: end-of-life metadata delists the application, and the maintainer documentation describes no way to remove or roll back an individual build (maintenance)

✅ ostree commit timestamp, stamped by the build service and read by flatpak remote-info: the clock of mpm’s per-app probe

VS Code Marketplace

vscode, vscodium

Unpublish, uniquely with client-side remediation: “the extension is removed from the Marketplace and added to a block list. If the extension has been installed, it’s automatically uninstalled by VS Code” (runtime security), against a public ledger of removals (RemovedPackages.md)

GitHub release assets

bin, bob, elan, getnf, roswell, gh-ext, soar, stew, and the asdf / mason / mise backends

None: withdrawing a build is the upstream author deleting their own release or tag. Nothing sits between them and the user

âś… server-set published_at on each release (REST API)

Upstream Git clones

antidote, hyprpm, antigen, fisher, lazy, oh-my-fish, sheldon, vim-pack, zim, zinit, zplug, yazi

None: a plugin is cloned straight from its upstream repository, so withdrawing a bad revision is its author force-pushing, retagging or deleting it themselves. No index sits in between

❌ a commit or tag date is set by the author and freely rewritable

Publisher-hosted signed feeds

zeroinstall

Feed edit: no index stands between a name and the code, a program being an XML feed its own publisher hosts and signs, so a version is withdrawn by that publisher dropping its implementation element and re-signing the document

❌ a feed’s released attribute is written by the publisher into the document they sign, so it is author-set

Claude Code marketplaces

claude-code-plugins

Manifest edit: a marketplace is a git repository listing its plugins, so a version is withdrawn by its owner dropping the entry or retagging, with no index in between

❌ neither the listing nor a marketplace manifest carries a publication date

Agent skill repositories

skills

Repository edit: a skill is a folder in a git repository the directory indexes, so it is withdrawn by its owner deleting the folder or the repository, with no release artifact in between

❌ the inventory carries no version at all, so there is nothing to date

PlatformIO Registry

platformio-core

Unpublish: pio pkg unpublish withdraws a pushed package from the registry, the one verb in the family that acts on what was published rather than on what is installed

âś… pio pkg search prints a per-package Published on date, server-set, though no declared operation reads it yet

ELPA archives

emacs

Archive drop: a package lives in an archive index its maintainers regenerate, so a version is withdrawn by the archive ceasing to list and serve it

❌ an archive entry carries a version and its dependencies, never a publication date

Julia General registry

julia

Yank: a registered version is withdrawn by a pull request against the registry marking it yanked, the sources staying wherever they were published

❌ a registry entry records a version, a tree hash and its dependencies, with no publication date

Pi package sources

pi

Upstream withdrawal: a package is a source spec resolved straight from npm or a git remote, so it is withdrawn wherever that source lives rather than through any index pi owns

❌ the listing records a spec and a path, with no version and no date

LURE recipe repositories

lure

Repository edit: a package is a recipe in a git repository LURE tracks, so it is withdrawn by that repository dropping the recipe

❌ a recipe records a version but no date

pkgit repositories

pkgit

Repository deletion: a package is a git repository named in the user’s own init.lua, so it is withdrawn by that repository disappearing or dropping its bldit.lua recipe

❌ a checkout is identified by HEAD alone, so there is nothing to date

Pearl package repositories

pearl

Repository edit: a package is an entry in a repository of recipes that clone from upstream, so it is withdrawn by that repository dropping the entry

❌ neither the listing nor a recipe records a version, so there is nothing to date

Zig release archive

zvm

Archive removal: a compiler is a tarball on ziglang.org or a mirror, so a version is withdrawn by that archive ceasing to serve it, with the index regenerated to match

❌ the catalog prints a version and a tag marker, never a publication date

Nim release archive

choosenim

Archive removal: a compiler is a tarball on nim-lang.org, so a version is withdrawn by that archive ceasing to serve it, with the tag list the catalog reads regenerated to match

❌ the catalog is the git tag list of nim-lang/Nim, whose dates whoever cut the tag sets rather than a server

pkgx pantry (dist.pkgx.dev)

pkgm

Pantry edit: a package is a per-platform versions.txt served from the distribution host, so a version is withdrawn by dropping its line and ceasing to serve the tarball behind it

❌ no surface prints a publication date, the distribution index listing versions alone

Go module proxy (pkg:golang)

go, gup

Retraction marker: a version stays served forever, the proxy being an immutable cache, so it is withdrawn by the author adding a retract directive to a newer go.mod that clients then skip

❌ no CLI surface prints one, though the proxy does serve a per-version .info timestamp

Source-based recipe trees

cave, emerge, pacstall, ports, prt-get, sorcery, spack

Index revert of the recipe tree

❌ a recipe carries no publication date

LVFS

fwupd

Index revert over staged remotes: firmware moves through embargo, testing and stable, and “You should only move stable firmware to testing and stable after completing an end-to-end test with the embargo remote” (LVFS). No withdrawal from stable is documented

SDKMAN broker

sdkman

Unpublish: the vendor release endpoint “supports POST and DELETE HTTP methods” (vendor API)

Mac App Store

mas

✅ server-set currentVersionReleaseDate in the catalog record mas lookup --json relays, stamped when a reviewed version goes live: the clock of mpm’s per-app probe (iTunes Search API)

Curated app stores and archives

steamcmd, miktex, tlmgr

No registry of their own

apm, apt-cyg, pkcon, sun-tools, swupd, topgrade, volta

âž– Not applicable: these managers either delegate to whichever manager they front (pkcon, topgrade) or wrap a retired project or a discontinued platform

âž–

Cross-referencing against the support table: every manager mpm gates today, and nearly every one with a shipped or proposed upstream mechanism, sits on a registry in the yank, unpublish or relabel rows. The exception is mise, which shipped minimum_release_age while resolving against GitHub release assets that have no retraction path at all. So the retraction shape is a good predictor of where a cooldown pays off, and only a rough predictor of where one gets built.

Limitations¶

The transitive-dependency gap¶

mpm’s cooldown is exactly as good as the underlying resolver’s. For managers that install with a real dependency resolver (PyPI, npm, …), the native mechanism applies the cutoff to the whole tree, including transitive dependencies. For managers without a native mechanism, mpm cannot retrofit one without reimplementing the resolver: pinning only the top-level package would leave transitive dependencies fresh, which is precisely the most common attack vector. That is why managers without a native gate are fail-closed rather than fail-open.

Managers whose install unit is a self-contained artifact, or whose whole transaction is enumerable up front with a native exclusion flag, sit outside this trap: there is no install-time dependency resolution to reimplement, so holding back the named packages is the whole gate. That property is the admission test for mpm’s per-package probe, and why the probe-gated rows of the support table carry one while the resolver ecosystems wait for their native mechanism.

Coverage limits¶

Distro and system managers (apt, dnf, pacman, brew, …) generally have no per-upstream publish date attached to a package version: their version string is the distro maintainer’s package build, not the upstream release, and the threat model differs (curated repositories with their own staging and review). The concept does not cleanly map. These managers are listed in the support table as N/A.

The retraction premise¶

The cooldown buys time for someone else to act. Where the registry has no way to withdraw a bad release (Retraction paths by registry), that time buys nothing: the compromised version simply ages into eligibility.

In an ecosystem whose only remediation is publishing a fixed version, the gate can go further and invert. A malicious 1.2.3 lands at T, the maintainer ships a clean 1.2.4 at T+3, and a 7-day cooldown holds 1.2.4 back until T+10 while 1.2.3 clears the window at T+7. For those three days the gate steers the resolver onto the compromised version, which an ungated run would have skipped in favor of the fix. A yank closes that window, since the resolver drops 1.2.3 the moment it is withdrawn; the exposure is specific to the index revert, flag only and none rows of the retraction table.

mpm does not detect this case. It injects the cutoff and lets the resolver decide.

The cooldown is authoritative, not a floor¶

mpm computes the cutoff from the requested --cooldown alone and injects it. It never reads the standing policy a user may already have configured natively, and the injected variable outranks that configuration in every supported ecosystem: UV_EXCLUDE_NEWER beats both user-level and project-level exclude-newer, and npm_config_min-release-age beats .npmrc.

So a --cooldown shorter than the user’s own setting silently relaxes it for the duration of the run:

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

$ uv pip install --dry-run "click-extra>=8.8.1"
hint: `click-extra` was filtered by `exclude-newer` […] v8.8.1, published 2026-08-02

$ UV_EXCLUDE_NEWER="1 day" uv pip install --dry-run "click-extra>=8.8.1"
 + click-extra==8.8.1

The word “cooldown” invites reading the option as a safety minimum, so the surprise runs in the unsafe direction. This is a second fail-open path, distinct from the fail-closed treatment of managers without a native gate described above: there, mpm refuses to act; here it acts with a weaker gate than the user asked the manager for.

Treating the option as a floor instead (taking the stricter of the two, or warning when --cooldown would relax a native setting) means reading each manager’s own configuration: uv.toml at two levels for uv, .npmrc at four for npm, each with its own format and precedence rules. That is the durable, manager-specific state the stateless gate deliberately forgoes, so for now the behavior is documented rather than changed. Users who rely on a strict standing policy should pass a --cooldown at least as long, or omit it and let the native setting apply untouched.

Read-only consistency¶

The outdated report is not filtered by the cooldown on managers without a native gate, so it may list versions that the subsequent upgrade would skip. For managers with one the same environment variable also affects outdated, so the report and the upgrade stay consistent.

The stateless-gate trade-off¶

mpm reads each release’s current age at run time and injects the cutoff: it keeps no ledger of past observations. That is safe (a fresh push looks fresh and is held back) but coarse. A package that publishes faster than the cooldown is held back wholesale until its newest version ages in, whereas a stateful tool can still install the most recent version that has already cleared the window. Because mpm trusts the age reported at the moment of the run, it also cannot cross-check that age against earlier observations the way aur-cooldown’s persisted version-to-commit ledger does. Both capabilities need the durable state mpm deliberately forgoes.

Possible future directions¶

Todo

  • Detect pipx’s internal pip (or uv) at runtime. mpm’s pip manager has a hard >=26.1.0 floor, but pipx maintains its own virtualenvs whose pip may be older or whose resolution may be routed through uv (where the right env var is UV_EXCLUDE_NEWER instead of PIP_UPLOADED_PRIOR_TO). Probing the resolver per venv would let mpm refuse to advertise enforcement when the underlying pip is stale.

  • Per-package exemptions (--cooldown-exclude). The gate is currently all-or-nothing per run, so one legitimately-fresh package (a security fix, a package the user publishes themselves) forces the cooldown off for the whole tree. Both enforcing managers already expose the escape hatch natively: uv’s --exclude-newer-package takes a PACKAGE=DATE pair, npm’s min-release-age-exclude takes names or minimatch globs. The catch is that this would not fit the uniform cooldown_env_var injection: uv publishes no environment variable for --exclude-newer-package (only the plain --exclude-newer carries a UV_EXCLUDE_NEWER binding), so uv needs the exemption appended to the command line while npm can keep taking it through npm_config_min-release-age-exclude. Supporting it therefore means a per-manager hook alongside the env var rather than a one-line addition, which is worth knowing before the option is designed.

  • Onboard mechanisms as they ship upstream. Several managers have active work that would slot into the cooldown_env_var framework as a one-line addition once released: Composer (composer/composer#12692), conda (conda/conda#15759), dnf5 (rpm-software-management/dnf5#2743), Scoop (ScoopInstaller/Scoop#6513), winget (microsoft/winget-cli#6178), VS Code extension installs (microsoft/vscode#321136).

  • Watch the three gates that shipped outside mpm’s scope. Cargo, Bundler and mamba each ship a release-age gate that the commands mpm drives never reach. Onboard each one if upstream widens it to cargo install, to gem install, or to the mamba and micromamba command lines. The notes under the support table record what each gate covers today.

  • Advisory mode for outdated on managers without a native gate. mpm could query each package registry directly (PyPI, RubyGems, crates.io, …) to annotate outdated with a “safe latest” column: purely informational, no install-side enforcement. This avoids the transitive-resolution trap while still being useful. It requires a new HTTP client surface and a state directory for date caching, neither of which mpm has today.

  • Consult a curated compromise-window denylist. aur-cooldown pairs its age gate with aur-malware-check’s campaigns.json, denying only the versions pushed inside a dated compromise window rather than freezing a package by name. This is orthogonal to release age: a version can be old enough to clear the cooldown yet still sit inside a known-bad window. Wiring such a feed into mpm would lean on the same HTTP client and state directory the advisory mode above already calls for.

  • Block-mode for the remaining bundled-artifact managers (snap, vscode). These install self-contained artifacts with no separate transitive resolution at install time, so the per-package hold shipped for flatpak and mas is sound for them too. The bottleneck is a server-set publish date the manager’s own CLI can reach: the VS Code Marketplace only answers over HTTP, and snap info prints no dates (snapd’s autonomous background refresh would bypass an mpm-side gate anyway).

Prior art¶