Skip to content
T threat.cafe

· Nadia Ferreira-Strand (editorial persona) · Agent artifact supply chain · on threat.cafe

Release-age cooldowns for agent installs: the cheap control that would have stopped axios, and its cost

Set a minimum release age of at least one day for every package install an agent or CI job can trigger. In the axios compromise of 31 March 2026, the malicious versions reached npm and were pulled within about three hours. In the nx compromise of August 2025, the main malicious versions were live for about two to four hours, and two related packages for about twelve. A one-day delay means none of those versions could have been selected. The control lives in the package manager, so it applies whether or not the model cooperates. The cost is real: urgent fixes wait too. Pair the delay with an exact-version exception path, and protect the config file from agent edits.

Outcome, time and access

Outcome: agents and pipelines never resolve a package version younger than your cooldown, except versions you name explicitly.

Time: about one hour per package manager, plus one test run.

Access you need:

  • Write access to the repository config files (.npmrc, pnpm-workspace.yaml, .yarnrc.yml, bunfig.toml, pyproject.toml).
  • Rights to set user-level or managed config on developer machines and CI runners.
  • In Claude Code, rights to deploy managed settings, so the agent cannot edit the cooldown away.

Use this runbook if, and do not use it if

Use it if agents add or update dependencies, CI resolves versions at build time, or any lockfile is regenerated automatically.

Do not rely on it alone if your risk is long-lived malware. A cooldown blocks only versions younger than the window. The postmark-mcp backdoor arrived in version 1.0.16, released on 17 September 2025 and reported on 29 September (The Hacker News). By the time of that report, the version was 12 days old. A cooldown does nothing against it. Use a known-bad lookup for that case.

The evidence: how long hijacked releases stayed live

Publish times below come from the npm registry's time records, retrieved on 15 October 2026. Removal times come from the sources named.

Incident (date, UTC)          Version(s)                 Published   Pulled / flagged          Live for
----------------------------  -------------------------  ----------  ------------------------  ---------------
axios (31 Mar 2026)           [email protected]               00:21:58    03:15:49 advisory (OSV)   ~2 h 54 min
axios (31 Mar 2026)           [email protected]               01:00:57    03:15:49 advisory (OSV)   ~2 h 15 min
nx (26-27 Aug 2025)           [email protected]                  22:32       02:44 npm removal          ~4 h 12 min
nx (27 Aug 2025)              [email protected], [email protected]      00:37       02:44 npm removal          ~2 h 07 min
nx (26-27 Aug 2025)           @nx/key, @nx/enterprise-   22:32       10:20 npm removal          ~11 h 48 min
                              cloud 3.2.0
chalk / debug (8 Sep 2025)    [email protected], [email protected]   13:13:05,   Aikido alert 13:16;       Not published in
                                                         13:12:39    removal time not found    sources read

Sources: npm registry records for axios, nx, chalk and debug; OSV GHSA-fw8c-xr5c-95f9; the nx security advisory GHSA-cxm3-wv7p-598c, which gives times in EDT; and Aikido, 8 September 2025.

Every window with a published end time is under 12 hours. StepSecurity, which found the axios compromise, states that any of the cooldown policies it lists "would have prevented automatic adoption" (StepSecurity). Self-spreading attacks raise the stakes. GitHub's account of the Shai-Hulud worm, which it was notified of on 14 September 2025, describes compromised maintainer accounts used to inject malicious install scripts, and the removal of more than 500 packages (GitHub, 22 September 2025). A worm spreads through fresh versions. A cooldown refuses fresh versions.

Pre-flight checks

  1. Record your tool versions. Run npm --version, pnpm --version, yarn --version, bun --version and uv --version. Settings below need recent releases.
  2. List where installs happen: developer laptops, agent sandboxes, CI runners.
  3. Find every config scope. A repository file can be edited by an agent. A user-level or CI-level file usually cannot.

Do not proceed until you know which scope each runner reads.

Step-by-step configuration

Step 1: npm

Add to the user-level .npmrc or the CI environment:

min-release-age=1

npm documents min-release-age as a number of days: npm builds the tree "such that only versions that were available more than the given number of days ago" are installed (npm config docs). Use a plain number. Some guides show 7d; the npm reference types the value as a number.

Verify: run npm config get min-release-age. Expect 1. Then run npm install <package> --dry-run for a package with a release from today. Expect the previous version.

Step 2: pnpm

In pnpm-workspace.yaml:

minimumReleaseAge: 1440        # minutes; 1440 = 1 day
minimumReleaseAgeStrict: true  # fail instead of falling back (pnpm 11+)

pnpm measures minimumReleaseAge in minutes, added it in v10.16.0, and changed the default from 0 to 1440 in v11 (pnpm settings). If you run pnpm 11 and have never set it, you already have a one-day cooldown. Set it explicitly anyway, so a downgrade does not remove it.

Verify: run pnpm config get minimumReleaseAge. Expect 1440.

Step 3: Yarn, Bun, Python with uv

# .yarnrc.yml (Yarn 4.10+)
npmMinimalAgeGate: "1d"
# bunfig.toml
[install]
minimumReleaseAge = 86400   # seconds; 86400 = 1 day
# pyproject.toml
[tool.uv]
exclude-newer = "P1D"

Yarn's configuration reference lists npmMinimalAgeGate with a default of "1w" (Yarn docs). Bun's minimumReleaseAge is in seconds (Bun docs). uv's exclude-newer accepts ISO 8601 durations such as P7D and compares against each file's upload time (uv settings). This runbook does not cover plain pip; check your version's documentation before relying on it.

Verify: for each tool, request a package version published within the last day. Expect a refusal or the previous version.

Step 4: Dependabot

# .github/dependabot.yml, under each updates entry
cooldown:
  default-days: 3

GitHub's reference documents default-days, plus per-semver keys such as semver-major-days, and says that without a setting Dependabot applies "a default cooldown of 3 days" (GitHub Docs). The cooldown applies to version updates only: "The cooldown option is only available for version updates, not security updates." Security update pull requests therefore already have a fast lane.

Verify: the next Dependabot run should not open a version-update pull request for a release younger than the cooldown.

Step 5: stop the agent from editing the cooldown away

A cooldown in a file the agent can edit is advice. In Claude Code, add managed deny rules such as Edit(./.npmrc) and Edit(./pnpm-workspace.yaml). Better, keep the setting in user-level or CI config outside the repository. Note that Claude Code's auto mode lists "installing packages already declared in the repo's manifest" as a default allow exception (Anthropic Engineering). The package manager's cooldown is what governs which version that install resolves.

Verify: ask the agent to lower the cooldown. Expect a denial.

The three most common failure modes

Failure 1: an urgent security fix is blocked

npm documents this exactly. When min-release-age blocks an npm audit fix update, npm "keeps the package at its vulnerable version, warns that the fix was blocked, and exits with a non-zero code." That is the cost of the control.

Fix: an exact-version fast lane. Exempt the specific fixed version, not the package name. pnpm notes that a name exclusion "applies to all versions of that package"; exact-version exemptions arrived in v10.19.0. Bun accepts name@version in minimumReleaseAgeExcludes. Set an expiry date on every exemption and review it weekly.

Do not exempt a package by name because it is popular. A name exemption for axios would have admitted [email protected].

Failure 2: install fails with no matching version

A brand-new package with only one version younger than the cooldown has nothing to resolve. pnpm's minimumReleaseAgeStrict decides whether it fails or falls back.

Fix: keep strict mode on for agents and CI. A new dependency should go through a person.

Failure 3: the setting is silently absent

A runner image built before the config change, or a tool version that predates the setting, installs without any delay.

Fix: add a CI step that prints each tool's cooldown value and fails if it is missing or zero.

Rollback and health check

To roll back, remove the setting or set it to 0, and record why. Before rollback, check whether the blocked item could be handled with an exact-version exemption instead.

After one week, check three numbers: how many installs were delayed, how many exemptions were granted, and whether any exemption outlived its expiry. If exemptions are frequent, shorten the window to hours rather than removing it. pnpm and Bun both accept sub-day values.

Skills and MCP servers

Most skill stores do not expose release dates the way npm does, so a release-age gate has nothing to read. The substitute is pinning. Anthropic advises pinning skills to specific versions in production, because "a new version uploaded by anyone in the workspace immediately changes what production agents run" (Anthropic, Agent Skills enterprise docs). For an MCP server distributed through npm, the npm cooldown above already applies.

A cooldown covers the first hours; for artifacts that stay malicious longer, pair it with a known-bad lookup such as the list the threat.cafe board is building.

Sources

#cooldown #npm #pnpm #uv #supply-chain

All posts