Why waiting before adoption can help
What Is Minimum Release Age?

Minimum release age means you refuse package versions that were published too recently.
Example: if your minimum age is 7 days, anything published in the last 7 days is blocked.
Why this matters:
- It reduces supply-chain risk from freshly compromised packages.
- It gives ecosystem scanners and maintainers time to detect bad releases.
- It lowers breakage from unstable just-published versions.
npm: .npmrc and a TanStack Incident
npm does not have a dedicated minimumReleaseAge setting like pnpm and Bun. The most modern npm-native approach is to combine:
- lockfile-first installs (
npm ci) - deterministic versioning in
.npmrc - npm’s
beforecutoff date when installing
Recommended .npmrc Baseline
save-exact=true
package-lock=true
engine-strict=true
This does not enforce age by itself, but it makes installs deterministic and safer.
TanStack Incident with npm before
During the recent TanStack Router supply-chain incident, some latest tags briefly pointed to compromised releases. before lets you cut resolution off before a risky publish window.
Sources:
- Incident thread: https://github.com/TanStack/router/issues/7383
- TanStack postmortem: https://tanstack.com/blog/npm-supply-chain-compromise-postmortem
before tells npm to resolve only versions published on or before a timestamp.
npm install @tanstack/react-router --before=2026-05-11T19:19:59.000Z
For a rolling 7-day window, compute the date in CI and pass it to npm.
CI-Friendly npm Pattern
{
"scripts": {
"install:secure": "node scripts/npm-install-with-min-age.mjs"
}
}
import { execSync } from 'node:child_process';
const MIN_AGE_DAYS = 7;
const cutoff = new Date(Date.now() - MIN_AGE_DAYS * 24 * 60 * 60 * 1000).toISOString();
execSync(`npm ci --before=${cutoff}`, { stdio: 'inherit' });
pnpm: pnpm-workspace.yaml
pnpm has first-class support for minimum release age.
minimumReleaseAge: 10080 # minutes (7 days)
minimumReleaseAgeExclude:
- '@my-org/*'
- 'typescript'
minimumReleaseAgeStrict: true
minimumReleaseAgeIgnoreMissingTime: false
Notes:
minimumReleaseAgeis in minutes.minimumReleaseAgeStrict: truemakes resolution fail when no acceptable aged version exists.minimumReleaseAgeIgnoreMissingTime: falseis stricter for registries that omit publish-time metadata.
Bun: bunfig.toml
Bun also supports minimum release age natively.
[install]
minimumReleaseAge = 604800 # seconds (7 days)
minimumReleaseAgeExcludes = ["@my-org/*", "typescript"]
frozenLockfile = true
Notes:
- Bun uses seconds, not minutes.
- Keep
frozenLockfile = truein CI to prevent accidental lockfile drift.
Pinned Versions and Lockfile SHAs
Minimum release age is strongest when paired with deterministic dependencies.
- Pin direct dependencies exactly (
1.2.3, not^1.2.3) for critical packages. - Enforce lockfile-only installs in CI:
npm ci,pnpm install --frozen-lockfile,bun install --frozen-lockfile. - Rely on integrity hashes in lockfiles to verify tarball content:
- npm:
integrityinpackage-lock.json - pnpm:
resolution.integrityinpnpm-lock.yaml - Bun: lockfile integrity metadata in
bun.lock/bun.lockb
- npm:
Enforcing It with a Claude Agent Skill
Add an agent skill that runs in CI and blocks unsafe dependency updates.
What the skill should do:
- Parse the lockfile (not just
package.json) to get exact resolved versions. - Query registry publish times for each resolved package version.
- Fail if any package is newer than policy (except allowlist).
- Comment on PRs with a clear report of violating packages.
Example policy output:
Blocked by minimum release age policy (7 days):
- @tanstack/react-router@1.169.8 published 0.5 days ago
- some-lib@4.2.0 published 0.7 days ago
Summary
- npm: use
.npmrcfor determinism plusnpm ci --before=<ISO date>for age gating. - pnpm: use native
minimumReleaseAgeinpnpm-workspace.yaml. - Bun: use native
install.minimumReleaseAgeinbunfig.toml. - Always pair age gating with pinned versions, frozen lockfiles, and lockfile hash verification.
Appendix: Copy-Paste Policy Files
.npmrc
save-exact=true
package-lock=true
engine-strict=true
pnpm-workspace.yaml
minimumReleaseAge: 10080 # 7 days
minimumReleaseAgeExclude:
- '@my-org/*'
- 'typescript'
minimumReleaseAgeStrict: true
minimumReleaseAgeIgnoreMissingTime: false
bunfig.toml
[install]
minimumReleaseAge = 604800 # 7 days
minimumReleaseAgeExcludes = ["@my-org/*", "typescript"]
frozenLockfile = true
package.json Script for npm Age Gating
{
"scripts": {
"install:secure": "node scripts/npm-install-with-min-age.mjs"
}
}
scripts/npm-install-with-min-age.mjs
import { execSync } from "node:child_process";
const MIN_AGE_DAYS = 7;
const cutoff = new Date(Date.now() - MIN_AGE_DAYS * 24 * 60 * 60 * 1000).toISOString();
execSync(`npm ci --before=${cutoff}`, { stdio: 'inherit' });
GitHub Actions CI Example
name: dependency-policy
on:
pull_request:
push:
branches: [main]
jobs:
policy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run install:secure
GitHub Actions CI Example (pnpm)
name: dependency-policy-pnpm
on:
pull_request:
push:
branches: [main]
jobs:
policy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
GitHub Actions CI Example (Bun)
name: dependency-policy-bun
on:
pull_request:
push:
branches: [main]
jobs:
policy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install --frozen-lockfile