Locking down dependency installs across npm, pnpm, yarn, and bun
A minimum release age (or “cooldown”) instructs a package manager to ignore any dependency version published less than a configured amount of time ago. The reference below covers the cooldown, install-time lifecycle script gating, and pnpm’s trust policy across the four major Node.js package managers; the setting names and units differ in each.
Table of Contents
Overview#
A cooldown delays a freshly published version from being installable. Compromised releases are usually flagged and removed from the registry within hours of publication, so even a 24-hour delay filters most of those incidents at the install layer without monitoring or scanning.
Each tool exposes the setting under its own name and unit of time:
- npm:
min-release-age, in days. npm CLI 11.10.0 (February 2026).[1]npm CLI documentation,min-release-age. - pnpm:
minimumReleaseAge, in minutes. pnpm 10.16 (September 2025); default1440since pnpm 11.0.[2]pnpm documentation,minimumReleaseAge. Notes the default of1440minutes since pnpm 11. - Yarn:
npmMinimalAgeGate, in minutes. Yarn Berry 4.10.0.[3]Yarn documentation,npmMinimalAgeGate. - Bun:
minimumReleaseAge, in seconds. Bun 1.3.0.[4]Bun documentation,install.minimumReleaseAge.
The setting can be written per-project or per-user. A per-project value protects the codebase against newly published policy violations; a user-wide value extends the cooldown to repositories that have not opted in. Both are worth setting; neither replaces the other, and the conditions under which either is silently bypassed are listed in the caveats below.
A cooldown is also only one of the install-time controls the package managers now ship. Lifecycle scripts covers disabling or allow-listing install-time script execution, and pnpm’s trust policy covers rejecting releases whose provenance regressed.
Per-project#
Per-project configuration lives in the package manager’s project-local config file and is committed to the repository. The selected tab is remembered across the rest of the post.
The setting belongs in the project’s .npmrc:
min-release-age=1
The value is in days. The setting cannot be combined with --before in the same invocation; npm raises an error if both are provided. An npm CLI version that does not recognize the key emits the following warning, which is cosmetic and does not prevent the setting from applying on a supporting CLI:
npm warn Unknown project config "min-release-age". This will stop working in the next major version of npm.
Since pnpm 11, the canonical location is pnpm-workspace.yaml:
minimumReleaseAge: 1440
Internal packages that should bypass the gate can be listed under minimumReleaseAgeExclude:
minimumReleaseAge: 1440
minimumReleaseAgeExclude:
- '@platformatic/*'
On pnpm 10.x, the same setting is written in .npmrc with a different casing:
minimum-release-age=1440
A known bug on pnpm 10.x causes minimumReleaseAge to be silently ignored when any package in the workspace sets shared-workspace-lockfile=false in its .npmrc.[8]pnpm issue #10008. minimumReleaseAge silently ignored on pnpm 10.x when any workspace package sets shared-workspace-lockfile=false. On 10.x, this is worth verifying before assuming the setting is honored.
The setting belongs in .yarnrc.yml (Yarn Berry 4.10 or later):
npmMinimalAgeGate: 1440
Packages that should bypass the gate can be listed under npmPreapprovedPackages:
npmMinimalAgeGate: 1440
npmPreapprovedPackages:
- '@platformatic/*'
npmPreapprovedPackages accepts both glob patterns and exact locators (for example, @aws-sdk/[email protected]), giving it more granularity than pnpm’s package-name-only exclude list.
The documentation states that a duration suffix such as "7d" is accepted, but a known parsing bug causes the suffix to be dropped.[9]Yarn issue #6991. Duration suffixes silently dropped when parsing npmMinimalAgeGate. Until that is resolved, a plain minute count is the reliable form: one day is npmMinimalAgeGate: 1440.
The setting belongs under [install] in bunfig.toml:
[install]
minimumReleaseAge = 86400
The value is in seconds. One day is 86400.
Packages that should bypass the gate can be listed under minimumReleaseAgeExcludes:
[install]
minimumReleaseAge = 86400
minimumReleaseAgeExcludes = ["@types/node", "typescript"]
User-wide#
User-wide configuration applies on a single machine, across every repository that does not override it.
npm config set min-release-age 1 --location=user
This writes to ~/.npmrc. Substituting global for user writes to the global npm configuration instead.
pnpm config set minimumReleaseAge 1440 --location=user
On pnpm 11 and later, non-auth settings written this way land in a global config.yaml whose location is platform-specific: ~/.config/pnpm/config.yaml on Linux, ~/Library/Preferences/pnpm/config.yaml on macOS, and ~/AppData/Local/pnpm/config/config.yaml on Windows. Setting $XDG_CONFIG_HOME moves the file to $XDG_CONFIG_HOME/pnpm/config.yaml on every platform, macOS included.[19]pnpm documentation, pnpm config. Lists the per-OS locations of the global config.yaml and its sibling rc file.
The platform split makes hand-editing risky: a config.yaml written to the Linux path on a Mac sits in a file pnpm never reads, and nothing warns about it. Auth and registry settings follow the same directory rules but go to a sibling rc file in INI format — and on pnpm 11, that INI file is consulted only for auth and registry keys, so a minimum-release-age= line carried over from pnpm 10 is silently ignored as well. Writing through pnpm config set --location=user targets the correct file on every OS, and running pnpm config get minimumReleaseAge from outside any project confirms the value is actually being read back.
yarn config set --home npmMinimalAgeGate 1440
--home writes to ~/.yarnrc.yml rather than the project-local file.
The same [install] table written in a user-wide bunfig applies across projects. Bun reads ~/.bunfig.toml — or $XDG_CONFIG_HOME/.bunfig.toml when that variable is set — and merges it with the project file, with the project file winning on conflicting keys:[20]Bun documentation, Global vs. local bunfig.toml. Enforcement of install.minimumReleaseAge from the user-wide file verified against Bun 1.3.7.
[install]
minimumReleaseAge = 86400
The merge is per-key, so a project bunfig.toml that sets an unrelated [install] option (for example, install.exact) still inherits the user-wide cooldown. A machine-wide layer below both files (/etc/bunfig.toml), aimed at fleet-level enforcement, is proposed in an open pull request.[10]Bun pull request #28727. Adds system-wide bunfig.toml support (/etc/bunfig.toml), merged beneath the user-wide and project files.
Lifecycle scripts#
A cooldown controls when a version becomes installable; it says nothing about what the package does once it lands. Install-time lifecycle scripts (preinstall, install, postinstall) run arbitrary shell commands during install, which makes them the primary payload mechanism in npm supply-chain attacks — the malicious code in a compromised release typically runs from a postinstall hook, not from anything the application imports. All four tools can disable these scripts outright or gate them behind a per-package allowlist. The granular options are recent additions, so the pinned-version caveat applies here just as much as it does to the cooldown.
The blunt instrument is ignore-scripts, which works on every npm version:
ignore-scripts=true
This disables lifecycle scripts for all dependencies, but it also affects the project’s own hooks: npm run, npm start, and npm test still execute the named script, but skip its pre- and post-scripts.
npm 11.16.0 adds a granular alternative: an allowScripts field in package.json, managed with npm approve-scripts and npm deny-scripts.[11]npm CLI documentation, npm approve-scripts. Manages the allowScripts field, available since npm 11.16.0. On npm 11 the field is advisory — unreviewed install scripts still run, and the install prints a warning listing them. Enforcement is opt-in:
strict-allow-scripts=true
npm v12 (estimated July 2026) makes enforcement the default: install scripts from dependencies will not run unless explicitly allowed, and git and remote-URL dependencies will not resolve unless --allow-git or --allow-remote is granted.[12]GitHub community discussion, "Preparing for npm v12: install scripts and non-registry sources become opt-in".
Since pnpm 10, dependency build scripts do not run unless approved. pnpm approve-builds walks through the dependencies that want to run them, and the approvals are recorded in the allowBuilds map (pnpm 10.26 and later; it replaces onlyBuiltDependencies and neverBuiltDependencies in pnpm 11):[13]pnpm documentation, allowBuilds. Added in pnpm 10.26; replaces onlyBuiltDependencies and neverBuiltDependencies in pnpm 11.
allowBuilds:
esbuild: true
core-js: false
Packages not listed are not built. strictDepBuilds (default true) fails the install with a non-zero exit code when any dependency has an unreviewed build script, which turns a warning into a CI gate. The escape hatch that runs everything without approval is named dangerouslyAllowAllBuilds, which is a fair summary of what it does.
enableScripts: false stops postinstall scripts from third-party packages; workspace packages still run their own:
enableScripts: false
false is the default since Yarn 4.14.0; every earlier release defaulted to true.[18]Yarn pull request #7089. Makes enableScripts: false the default; released in Yarn 4.14.0. Existing projects are migrated with an explicit enableScripts: true. The 4.14 migration also preserves old behavior by writing an explicit enableScripts: true into existing projects, so a repository upgraded across the flip stays opted in until that line is removed. Both conditions argue for setting false explicitly rather than relying on the default. Individual packages are opted back in through dependenciesMeta in package.json, which acts as an allowlist while scripts are globally disabled:[14]Yarn documentation, enableScripts and dependenciesMeta.
{
"dependenciesMeta": {
"esbuild": { "built": true }
}
}
Yarn also ships enableHardenedMode, which re-validates lockfile resolutions and checksums against the remote registry during install. It is enabled automatically on pull requests from public GitHub repositories and can be forced on for any branch edited from outside the circle of trust.[15]Yarn documentation, enableHardenedMode. Enabled automatically on pull requests from public GitHub repositories.
Bun is default-secure here: lifecycle scripts only run for packages on a curated default trusted list, and only when those packages are installed from npm. Additional packages are trusted via trustedDependencies in package.json:[16]Bun documentation, Lifecycle scripts. Covers trustedDependencies and the default trusted list.
{
"trustedDependencies": ["node-sass"]
}
Defining the field replaces the default list rather than extending it, so an explicit list must re-include any default-trusted packages whose scripts are still needed (sharp, esbuild, and similar). Setting trustedDependencies: [] opts out of the default list entirely. Disabling scripts wholesale is also available:
[install]
ignoreScripts = true
pnpm’s trust policy#
pnpm 10.21 adds a check orthogonal to both the cooldown and script gating. With trustPolicy set to no-downgrade, the install fails if a package’s trust level has decreased relative to its earlier releases — for example, a package whose previous versions carried provenance attestations suddenly shipping a version without one.[17]pnpm documentation, trustPolicy. Added in pnpm 10.21, alongside trustPolicyExclude (10.22) and trustPolicyIgnoreAfter (10.27). That signature is characteristic of a stolen-token compromise: the attacker publishes from their own machine, and the provenance that the maintainer’s CI pipeline would have attached is missing.
trustPolicy: no-downgrade
Two companion settings handle false positives. trustPolicyExclude (pnpm 10.22) takes package selectors to skip, for packages that legitimately changed how they publish. trustPolicyIgnoreAfter (pnpm 10.27) skips the check for versions older than a given number of minutes — the cooldown’s reasoning applied in reverse: a release that has been public for weeks without being pulled is unlikely to be the compromise this check exists to catch. Like the cooldown, the setting can be written per-project in pnpm-workspace.yaml or user-wide with pnpm config set trustPolicy no-downgrade --location=user.
Caveats#
A handful of conditions can quietly bypass the configured cooldown or change what is actually being honored.
The check runs at install time, not when an updater opens a PR. Renovate and Dependabot evaluate updates independently of the package manager’s cooldown, so their own cooldown options must be set separately. Both ship reasonable defaults: Renovate’s config:best-practices preset waits three days before raising an npm update,[5]Renovate documentation, config:best-practices preset. Includes security:minimumReleaseAgeNpm, which waits three days before raising an npm update. and Dependabot exposes cooldown.default-days with semver-specific overrides.[6]GitHub Docs, Dependabot cooldown configuration, covering default-days and the semver-specific overrides.
A repository’s pinned package manager is what actually runs the install. A pin via the packageManager field, Corepack, Volta, a Docker base image, or setup-node with version-file overrides whatever is installed globally. If the pinned version predates cooldown support, every layer of configuration above it is inert. The most reliable check is to run pnpm --version (or the equivalent) from inside the repository, or to inspect the setup-node step in the CI log.
Project-level configuration takes precedence over user-level configuration in all four tools. A user-wide cooldown of one day does not apply inside a repository whose project config sets a smaller value or none at all. A user-wide value is not a backstop for repositories that have not opted in.
The user-wide file lives at a different path per operating system for pnpm and Bun. npm and Yarn read ~/.npmrc and ~/.yarnrc.yml from the home directory everywhere, but pnpm’s global config.yaml moves between ~/.config/pnpm/ (Linux), ~/Library/Preferences/pnpm/ (macOS), and ~/AppData/Local/pnpm/config/ (Windows),[19]pnpm documentation, pnpm config. Lists the per-OS locations of the global config.yaml and its sibling rc file. and Bun switches from ~/.bunfig.toml to $XDG_CONFIG_HOME/.bunfig.toml when the variable is set. A config file hand-written to the path from another operating system’s documentation is silently ignored. Writing through the tool’s own CLI and then reading the value back from outside any project confirms the setting landed somewhere it will actually be read.
None of the four tools support per-registry scoping. Internal packages from a private registry are subject to the cooldown unless explicitly listed in the per-tool exclusion option. npm does not yet expose one; an issue is open against the CLI tracking it.[7]npm CLI issue #8994. Tracks an exclusion list for min-release-age.
Defaults are shifting upward. pnpm 11 ships with the gate enabled at 1440 minutes by default,[2]pnpm documentation, minimumReleaseAge. Notes the default of 1440 minutes since pnpm 11. and npm v12 is set to make install scripts and non-registry dependency sources opt-in.[12]GitHub community discussion, "Preparing for npm v12: install scripts and non-registry sources become opt-in". Setting explicit values insulates the configured behavior against changes elsewhere in the ecosystem.
Acknowledgements#
This digest closely follows two gists that did the original work of pulling the per-tool settings together:
- Matteo Collina, “Configuring minimum release age across npm, pnpm, and yarn” (April 2026). The original write-up covering npm, pnpm, and Yarn.
- Tom Ricci, “Configuring minimum release age across npm, pnpm, yarn, and bun”. Extended Collina’s notes with Bun coverage and the per-tool exclusion options.