Code,  Security

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 before cutoff 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:

  • minimumReleaseAge is in minutes.
  • minimumReleaseAgeStrict: true makes resolution fail when no acceptable aged version exists.
  • minimumReleaseAgeIgnoreMissingTime: false is 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 = true in 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: integrity in package-lock.json
    • pnpm: resolution.integrity in pnpm-lock.yaml
    • Bun: lockfile integrity metadata in bun.lock/bun.lockb

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:

  1. Parse the lockfile (not just package.json) to get exact resolved versions.
  2. Query registry publish times for each resolved package version.
  3. Fail if any package is newer than policy (except allowlist).
  4. 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 .npmrc for determinism plus npm ci --before=<ISO date> for age gating.
  • pnpm: use native minimumReleaseAge in pnpm-workspace.yaml.
  • Bun: use native install.minimumReleaseAge in bunfig.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
Comentarios desactivados en Why waiting before adoption can help