> For the complete documentation index, see [llms.txt](https://docs.heeler.com/mrecEO40m5D6bt7Pq5pE/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.heeler.com/mrecEO40m5D6bt7Pq5pE/prevent/package-manager-cooldown.md).

# Package-Manager Cooldown

Configure your package managers and CI to refuse packages published too recently — enforcing a minimum release age at install time, before a PR is ever opened.

Most supply-chain attacks arrive through a package version published in the last day or two. Modern package managers can refuse to install anything newer than a **minimum release age**, which shuts that window at the point of installation — on developer laptops and CI runners, before a pull request even exists.

This is the client-side complement to Heeler's server-side controls. Used together they form a complete defense:

* **Package-manager cooldown** (this page) — blocks a too-new version from being installed at all.
* [**Dependency Version Minimum Age guardrail**](/mrecEO40m5D6bt7Pq5pE/prevent/pr-guardrails/guardrail-types/dependency-sca-guardrails.md) — evaluates every PR and flags a too-new version that reached a lockfile before it can merge.
* [**Auto-fix minimum package age**](/mrecEO40m5D6bt7Pq5pE/administer-and-monitor/program-policy/remediation-assignment-and-routing.md#package-freshness-gating) — keeps Heeler's own remediation from recommending a version that's too new to install.

{% hint style="info" %}
**Choosing a window — 3 vs. 7 days.** Set your cooldown to match your [Dependency Version Minimum Age guardrail](/mrecEO40m5D6bt7Pq5pE/prevent/pr-guardrails/guardrail-types/dependency-sca-guardrails.md) threshold so there's no gap between what Heeler flags in PRs and what your package managers will install. The median time-to-detection for malicious npm packages is under 48 hours, so **3 days** catches the vast majority of attacks with minimal friction; **7 days** is broader but delays legitimate hotfixes — a CVE patch can't be installed for a week. Pick based on your security-response needs.
{% endhint %}

## Quick start

Complete coverage is four steps:

1. **Commit the config file to every repo root.** This is the primary control — it applies automatically in CI and on every developer machine. The right file depends on the manager (see below).
2. **Set the global config on developer laptops** to cover installs outside a managed repo. Deploy via MDM or config management; add it to your bootstrap script for new machines.
3. **Match Heeler auto-fix** — set [Minimum Package Age](/mrecEO40m5D6bt7Pq5pE/administer-and-monitor/program-policy/remediation-assignment-and-routing.md#package-freshness-gating) to the same threshold, so Auto-fix only recommends versions old enough to install.
4. **Pin your package-manager version** — the setting is silently ignored on older versions. See [Pinning your package-manager version](#pinning-your-package-manager-version).

If you use **Renovate or Dependabot**, configure the same cooldown there too — otherwise they'll open PRs your package manager then refuses to install.

## Supported package managers

| Package manager | Min version | Config file                  | Setting                |
| --------------- | ----------- | ---------------------------- | ---------------------- |
| npm             | v11.10.0+   | `.npmrc`                     | `min-release-age`      |
| pnpm            | v10.16+     | `.npmrc`                     | `minimum-release-age`  |
| Yarn            | v4.10+      | `.yarnrc.yml`                | `npmMinimalAgeGate`    |
| Bun             | v1.3+       | `bunfig.toml`                | `minimumReleaseAge`    |
| uv              | v0.9.17+    | `pyproject.toml` / `uv.toml` | `exclude-newer`        |
| pip             | v26.1+      | `pip.conf`                   | `uploaded-prior-to`    |
| Deno            | v2.6+       | `deno.json` / `deno.jsonc`   | `minimumDependencyAge` |

{% hint style="warning" %}
These settings are **silently ignored on older versions** of each package manager — enforce a minimum version too ([below](#pinning-your-package-manager-version)).
{% endhint %}

## Configuration

{% tabs %}
{% tab title="npm" %}

```ini
# .npmrc
min-release-age=7d
```

* Applies to all packages — npm has no per-package exclusions yet.
* Accepts duration strings: `7d`, `72h`.
* Covers **direct** dependencies; transitive coverage is weaker than pnpm — a real gap, since most supply-chain attacks arrive transitively.
  {% endtab %}

{% tab title="pnpm" %}
pnpm reads `.npmrc` (not a separate file):

```ini
# .npmrc
minimum-release-age=7d
minimum-release-age-exclude=esbuild,turbo
```

* Covers **direct and transitive** dependencies.
* By default, a too-new version **silently falls back** to the next older one that satisfies the threshold — the install succeeds at an unexpected version with no error. Set `minimumReleaseAgeStrict=true` to hard-fail instead.
* Keep `minimum-release-age-exclude` short and review it regularly.
  {% endtab %}

{% tab title="Yarn" %}

```yaml
# .yarnrc.yml
npmMinimalAgeGate: "7d"
npmPreapprovedPackages:
  - "@yourorg/*"
```

* Accepts duration strings (`7d`, `168h`) or an integer number of minutes.
* **Errors** when the gate fires — no silent fallback.
* Use `npmPreapprovedPackages` (glob patterns) to pre-approve your own org's packages.
  {% endtab %}

{% tab title="Bun" %}

```toml
# bunfig.toml
[install]
minimumReleaseAge = 604800  # 7 days, in seconds
```

* Value is in **seconds**, not a duration string (7 days = 604800).
* No per-package exclusions yet.
  {% endtab %}

{% tab title="uv" %}

```toml
# pyproject.toml  ([tool.uv]) — or uv.toml ([uv])
[tool.uv]
exclude-newer = "2025-01-01T00:00:00Z"
```

{% hint style="warning" %}
In a config file, `exclude-newer` requires an **absolute RFC 3339 timestamp** — relative durations (`"7 days"`) work only as the CLI flag `--exclude-newer`. An absolute timestamp goes stale and must be refreshed (e.g. a scheduled CI job or Renovate). For a rolling window without manual updates, compute it at install time via the environment variable: `UV_EXCLUDE_NEWER=$(date -u -d "7 days ago" +%Y-%m-%dT%H:%M:%SZ)`.
{% endhint %}

* Per-package exclusions via `exclude-newer-package` in the config file.
* Compares against each artifact's **upload timestamp** on PyPI, not the release date.
  {% endtab %}

{% tab title="pip" %}

```ini
# pip.conf  (Linux/macOS: ~/.config/pip/pip.conf or /etc/pip.conf; Windows: %APPDATA%\pip\pip.ini)
[install]
uploaded-prior-to = P7D
```

* Requires pip **v26.1+** (26.0 accepted only absolute timestamps; the relative ISO 8601 duration `P7D` = 7 days arrived in 26.1).
* Needs the index to expose upload-time metadata — PyPI does; private/self-hosted indexes may not, and pip errors if it's unavailable.
* No per-package exclusions.
  {% endtab %}

{% tab title="Deno" %}

```json
{
  "minimumDependencyAge": {
    "age": "7d",
    "exclude": ["npm:@yourorg/internal-package", "jsr:@yourorg/internal-package"]
  }
}
```

* Introduced in Deno v2.6; accepts duration strings (`7d`, `168h`).
* Per-package exclusions via the `exclude` array (`npm:` / `jsr:` prefixes).
* Applies to both npm and JSR packages.
  {% endtab %}
  {% endtabs %}

## Where to apply it

Getting the setting in place is only half the job — it has to apply everywhere packages get installed.

### Project config (highest priority)

Committing the config file to the **repo root** is the single most impactful step: it applies to everyone who clones the repo — developers, CI, automation — with no per-machine setup. For **monorepos**, place it at the workspace root (most managers propagate it to all workspace packages; if workspaces use different managers, each manager's file must be present). If you already have a `.npmrc` for other settings, just append the line — it won't conflict.

### Global config (developer laptops)

Global config catches installs outside a managed repo:

```bash
npm config set min-release-age 7d --global
pnpm config set minimum-release-age 7d --global
echo 'npmMinimalAgeGate: "7d"' >> ~/.yarnrc.yml   # Yarn
```

Deploying to existing machines, most reliable first: **MDM (Jamf/Intune)** pushes it immediately to enrolled machines; **config management (Ansible/Chef/Puppet/Salt)** converges on next run; a **bootstrap/dotfiles re-run** is low-overhead but relies on compliance. Verify with `npm config get min-release-age` (expect `7d`).

{% hint style="info" %}
Global laptop config is belt-and-suspenders. The **project-level file committed to each repo** is the primary control — get that in place across your repos first.
{% endhint %}

### CI runners

If the project config is committed, CI picks it up automatically — with one critical caveat: **`npm ci` installs strictly from the lockfile and the age gate does not fire.** `pnpm install --frozen-lockfile` and `yarn install --immutable` behave the same. In CI, the [Dependency Version Minimum Age guardrail](/mrecEO40m5D6bt7Pq5pE/prevent/pr-guardrails/guardrail-types/dependency-sca-guardrails.md) is the operative control. For extra assurance, assert the config is present as a pre-install step:

```yaml
# GitHub Actions (adapt the file/setting to your manager)
- name: Assert minimum release age is configured
  run: grep -q "min-release-age" .npmrc || (echo "min-release-age not set in .npmrc" && exit 1)
```

The same one-line `grep` assertion works in GitLab CI, Bitbucket Pipelines, and Azure Pipelines.

## Keep Renovate / Dependabot in step

If you automate dependency updates, apply the same cooldown there or they'll propose versions your package manager refuses to install (causing CI failures):

* **Renovate** — set `minimumReleaseAge` in `renovate.json`.
* **Dependabot** — set `cooldown.default-days` in `.github/dependabot.yml`. (Better still, switch to Heeler [auto-fix](/mrecEO40m5D6bt7Pq5pE/fix/automate-remediation.md).)

## Overriding the gate for urgent installs

Every supported manager has a per-invocation bypass — e.g. installing a security patch released less than your window ago:

```bash
npm install <package> --min-release-age=0                       # npm
pnpm install --config.minimum-release-age-exclude=<package>     # pnpm
# Yarn: add the version to npmPreapprovedPackages in .yarnrc.yml, then revert after install
```

Document overrides when they happen — an unexpected bypass is exactly what warrants a note in the PR.

## Pinning your package-manager version

Because the setting is silently ignored on older versions, enforce a minimum. There's no single mechanism across all managers:

* **npm & Yarn** — `engines` in `package.json` **plus** `engine-strict=true` in `.npmrc` to hard-fail:

  ```json
  { "engines": { "npm": ">=11.10.0" } }
  ```

  (Yarn Berry reads `engines` only via the separately installed `yarn-plugin-engines`.)
* **pnpm** — `devEngines.packageManager` in `package.json` (`{"name":"pnpm","version":">=10.16.0","onFail":"error"}`), enforced natively.
* **All three** — the `packageManager` field + **Corepack** pins an *exact* version. Enable with `corepack enable`. Bun isn't supported by Corepack.

{% hint style="warning" %}
Corepack was removed from Node.js 25+; install it separately there: `npm install -g corepack`.
{% endhint %}

## Managers without native support

For any team on a manager that lacks a cooldown setting, the [Dependency Version Minimum Age guardrail](/mrecEO40m5D6bt7Pq5pE/prevent/pr-guardrails/guardrail-types/dependency-sca-guardrails.md) is the backstop — it flags any too-new version introduced into a lockfile at PR time. Enforce client-side where you can, and rely on the guardrail to catch the rest.

## Lockfile blind spots

{% hint style="warning" %}
If a too-new package is **already in your lockfile** — say, added by an update PR — some managers install it straight from the lockfile without re-checking the age gate. The setting only fires on **net-new resolutions** (and `npm ci` never fires it). This is exactly where the [Dependency Version Minimum Age guardrail](/mrecEO40m5D6bt7Pq5pE/prevent/pr-guardrails/guardrail-types/dependency-sca-guardrails.md) is the complement: it evaluates every PR and catches a too-new version that reached the lockfile by any path, before it merges.
{% endhint %}

## Related

* [Dependency Version Minimum Age guardrail](/mrecEO40m5D6bt7Pq5pE/prevent/pr-guardrails/guardrail-types/dependency-sca-guardrails.md) — the PR-time complement to this install-time control.
* [Build a Supply Chain Security Program](/mrecEO40m5D6bt7Pq5pE/solutions-and-use-cases/building-a-proactive-supply-chain-security-program.md) — where this fits in the bigger picture.
* [Auto-fix minimum package age](/mrecEO40m5D6bt7Pq5pE/administer-and-monitor/program-policy/remediation-assignment-and-routing.md#package-freshness-gating) — keep Heeler's remediation consistent with your cooldown.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.heeler.com/mrecEO40m5D6bt7Pq5pE/prevent/package-manager-cooldown.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
