A compromised package is most dangerous in the hours between its publication and
its yanking. The cheapest defence available to us is patience: refuse to install
anything the ecosystem has not had a few days to look at. Dependabot already does
this — .github/dependabot.yml carries cooldown.default-days: 3 on all three
ecosystems — but Dependabot is only one of the ways a version gets chosen here. A
developer typing pnpm add, a uv add, a uv pip install in CI, and a Docker
image build all pick versions with no waiting period at all.
This issue is to apply one cooldown rule — three days — everywhere a version is
chosen, and to make the rule structural rather than something a person has to
remember to type.
What the survey found
Three things were verified against the pinned toolchains (uv 0.9.13, pnpm 10.30.2)
rather than assumed, and each one shapes the design:
-
A cooldown is a resolution-time control, not an install-time one. Both
install-from-lock paths ignore it: pnpm install --frozen-lockfile installs a
locked version that violates the cooldown without complaint, and uv sync --frozen audits and moves on. That is correct behaviour, not a hole — the
lockfile is the reviewed artifact. The cooldown belongs where versions are
picked.
-
UV_EXCLUDE_NEWER on an install path silently discards the lockfile. Set
it on a plain uv sync and uv answers Ignoring existing lockfile due to addition of timestamp cutoff, re-resolves, and rewrites uv.lock. So the
naive "export it globally in CI" approach would have quietly stopped CI from
testing the pinned dependency set. The cooldown must be scoped to the commands
that resolve, and the install paths must be pinned so they cannot resolve.
-
uv has no rolling cooldown setting. --exclude-newer accepts RFC 3339
timestamps and dates only — 3 days ago, 3d and P3D are all rejected — and
there is no --minimum-release-age equivalent. pnpm does have one
(minimumReleaseAge, in minutes, honoured by 10.30.2 and verified to refuse a
too-new version with ERR_PNPM_NO_MATURE_MATCHING_VERSION). The asymmetry is
why Python needs a small wrapper and Node needs one config line.
Scope
- pnpm — declare
minimumReleaseAge in pnpm-workspace.yaml so every
pnpm add/pnpm update, local or in Docker, is covered without a flag.
- uv — one script owning the number and the rolling cutoff, wrapping the
commands that resolve (uv add, uv lock, uv pip install, uv build).
- Harden the install paths so a re-resolve cannot happen behind the cooldown's
back: CI's uv sync becomes uv sync --locked, which both refuses to resolve
and gates lockfile drift.
- Docker — the images inherit the pnpm setting through the copied workspace
file; add the docker ecosystem to Dependabot, which has no cooldown today
because it has no entry today, and pin the one floating :latest tag in
docker/compose.yaml (a floating tag is the one thing no cooldown can protect).
- Node is pnpm only.
scripts/check.sh invokes npx playwright test twice;
those become pnpm exec. This is already the stated rule in the nodejs-setup
skill ("pnpm only (never npm, never yarn)") and check.sh is the exception.
- A gate, in
tests/scripts/, so none of the above can rot back.
- Documentation of the rule and — more importantly — of when to override it,
since a cooldown that cannot be escaped becomes a cooldown people disable.
Explicitly out of scope
- apt (
ffmpeg in CI and in docker/api.Dockerfile). Debian stable is a
frozen, signed snapshot already; a three-day rule over it would be theatre.
- GitHub Actions are already covered by the existing Dependabot entry.
- Security updates stay immediate, in every ecosystem. A cooldown that delays a
fix for a known-exploited hole is worse than no cooldown.
A compromised package is most dangerous in the hours between its publication and
its yanking. The cheapest defence available to us is patience: refuse to install
anything the ecosystem has not had a few days to look at. Dependabot already does
this —
.github/dependabot.ymlcarriescooldown.default-days: 3on all threeecosystems — but Dependabot is only one of the ways a version gets chosen here. A
developer typing
pnpm add, auv add, auv pip installin CI, and a Dockerimage build all pick versions with no waiting period at all.
This issue is to apply one cooldown rule — three days — everywhere a version is
chosen, and to make the rule structural rather than something a person has to
remember to type.
What the survey found
Three things were verified against the pinned toolchains (uv 0.9.13, pnpm 10.30.2)
rather than assumed, and each one shapes the design:
A cooldown is a resolution-time control, not an install-time one. Both
install-from-lock paths ignore it:
pnpm install --frozen-lockfileinstalls alocked version that violates the cooldown without complaint, and
uv sync --frozenaudits and moves on. That is correct behaviour, not a hole — thelockfile is the reviewed artifact. The cooldown belongs where versions are
picked.
UV_EXCLUDE_NEWERon an install path silently discards the lockfile. Setit on a plain
uv syncand uv answersIgnoring existing lockfile due to addition of timestamp cutoff, re-resolves, and rewritesuv.lock. So thenaive "export it globally in CI" approach would have quietly stopped CI from
testing the pinned dependency set. The cooldown must be scoped to the commands
that resolve, and the install paths must be pinned so they cannot resolve.
uv has no rolling cooldown setting.
--exclude-neweraccepts RFC 3339timestamps and dates only —
3 days ago,3dandP3Dare all rejected — andthere is no
--minimum-release-ageequivalent. pnpm does have one(
minimumReleaseAge, in minutes, honoured by 10.30.2 and verified to refuse atoo-new version with
ERR_PNPM_NO_MATURE_MATCHING_VERSION). The asymmetry iswhy Python needs a small wrapper and Node needs one config line.
Scope
minimumReleaseAgeinpnpm-workspace.yamlso everypnpm add/pnpm update, local or in Docker, is covered without a flag.commands that resolve (
uv add,uv lock,uv pip install,uv build).back: CI's
uv syncbecomesuv sync --locked, which both refuses to resolveand gates lockfile drift.
file; add the
dockerecosystem to Dependabot, which has no cooldown todaybecause it has no entry today, and pin the one floating
:latesttag indocker/compose.yaml(a floating tag is the one thing no cooldown can protect).scripts/check.shinvokesnpx playwright testtwice;those become
pnpm exec. This is already the stated rule in thenodejs-setupskill ("pnpm only (never npm, never yarn)") and
check.shis the exception.tests/scripts/, so none of the above can rot back.since a cooldown that cannot be escaped becomes a cooldown people disable.
Explicitly out of scope
ffmpegin CI and indocker/api.Dockerfile). Debian stable is afrozen, signed snapshot already; a three-day rule over it would be theatre.
fix for a known-exploited hole is worse than no cooldown.