Loading…

No package manager supports minimum release age natively — but you can enforce it. Here's a complete guide for npm, pnpm, Yarn, Bun, and Deno, plus a universal CI script that works everywhere.
🛡️ Security tip: One of the most underrated defenses against npm supply chain attacks is requiring packages to be a minimum number of days old before your toolchain will install them. Here's how to do it across every major package manager.
After the recent wave of npm supply chain attacks — including the TanStack compromise and dozens of other malicious packages — the community is rethinking how we trust the packages we install.
One effective, low-friction mitigation is minimum release age: a policy that prevents your project from installing a package version that was published less than N days ago. The logic is simple — most malicious packages are discovered within 24–72 hours of publication by the community, security researchers, or automated scanners. If your toolchain waits a few days before allowing a new version, you dramatically reduce your attack surface.
The challenge? Every package manager handles this differently — and most don't support it natively at all.
This guide walks through the current state for npm, pnpm, Yarn, Bun, and Deno, and provides a universal CI-based solution you can use today regardless of which manager you're on.
Day 0 → Malicious package version published to npm
Day 0 → Developers with auto-update configs install it
Day 0 → Secrets and tokens start being exfiltrated
Day 1 → Security researcher notices unusual package behavior
Day 1 → Report filed with npm security team
Day 2 → Package pulled from registry
Day 2 → Advisory published
Day 3+ → Developers who pinned versions or had minimum age policies: unaffected ✅Newly published malicious packages — attackers rely on rapid installs
Compromised maintainer accounts pushing bad versions
Automated bots that immediately install latest
Dependency confusion attacks targeting internal package names
Packages that were already in your lock file before the attack
Long-term sleeper packages (malicious code added after months of legitimate use)
Packages you've explicitly pinned to a specific (malicious) version
npm doesn't have a native minimum release age setting, but you can layer it in through several mechanisms.
Socket.dev offers the most frictionless solution with its npm wrapper:
# Install Socket CLI
npm install -g @socketsecurity/cliUse socket instead of npm — it warns about new/risky packages
socket npm install
Socket analyzes package age, network access, install scripts, and dozens of other signals before allowing installation.
.npmrc with Audit-First WorkflowWhile .npmrc doesn't support age natively, you can enforce a workflow:
# .npmrc
audit=true
audit-level=moderate
fund=falseCombine with a pre-commit or pre-install hook that checks ages via the registry API (see the Universal CI Solution below).
npm now supports package provenance — cryptographic proof linking a package to its source repo and CI build. Enable it in your workflow:
npm audit signaturesThis won't block new packages but will flag any package without valid provenance.
pnpm has stronger security defaults than npm and offers better hooks for enforcing policies.
onlyBuiltDependencies (pnpm v9+)Instead of blocking all new packages, you can whitelist which packages are allowed to run install scripts:
// package.json
{
"pnpm": {
"onlyBuiltDependencies": ["esbuild", "node-gyp", "sharp"]
}
}This prevents newly-compromised packages from running malicious postinstall scripts, which is often the actual attack vector.
pnpm auditpnpm audit --audit-level moderate
pnpm audit --fix # auto-apply safe fixesAlways use --frozen-lockfile in CI to prevent unexpected dependency resolution:
pnpm install --frozen-lockfilepnpm supports lifecycle hooks in .pnpmfile.cjs:
// .pnpmfile.cjs
function readPackage(pkg, context) {
// Log all packages being installed for audit trail
context.log(Installing: ${pkg.name}@${pkg.version});
return pkg;
}
module.exports = { hooks: { readPackage } };
Yarn Berry (v2+) has the most powerful policy enforcement system through constraints and plugins.
Constraints allow you to write rules (in a Prolog-like DSL or JavaScript) that every workspace must satisfy:
// yarn.config.cjs
module.exports = {
async constraints({ Yarn }) {
for (const dep of Yarn.dependencies()) {
// Enforce exact versions only (no ^ or ~ ranges)
if (dep.range.startsWith('^') || dep.range.startsWith('~')) {
dep.update(dep.range.slice(1));
}
}
}
};Run with:
yarn constraints
yarn constraints --fixyarn npm audit
yarn npm audit --all --recursiveyarn install --immutableBun is the newest entrant and has the most minimal security tooling currently, but it's improving rapidly.
bun install --frozen-lockfilebun pm auditBun respects the --ignore-scripts flag:
bun install --ignore-scriptsYou can also set this project-wide in bunfig.toml:
[install]
ignoreScripts = trueDeno takes a fundamentally different approach to the dependency problem that makes many of these issues less relevant by default.
// Pinned to an exact version — no floating ranges
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";
import { z } from "npm:zod@3.23.4";// deno.json
{
"imports": {
"zod": "npm:zod@3.23.4",
"hono": "npm:hono@4.4.0"
}
}Deno's permission system limits what a dependency can do at runtime:
# Only grant the specific permissions your app needs
deno run --allow-net=api.myapp.com --allow-read=./data app.tsA malicious dependency trying to exfiltrate env vars or write to disk would be blocked by the permission sandbox.
Regardless of which package manager you use, this CI script works universally. It queries the npm registry for every dependency and fails the build if any package was published within the last 3 days.
// scripts/check-package-age.mjs
import { readFileSync } from 'fs';
const MIN_AGE_DAYS = 3;
const LOCKFILE_PATH = './package-lock.json';
async function checkPackageAge(name, version) {
const res = await fetch(https://registry.npmjs.org/${name});
if (!res.ok) return; // Skip if registry unreachable
const data = await res.json();
const publishedAt = data.time?.[version];
if (!publishedAt) return;
const ageInDays = (Date.now() - new Date(publishedAt)) / (1000 * 60 * 60 * 24);
if (ageInDays < MIN_AGE_DAYS) {
console.error(❌ Package too new: ${name}@${version});
console.error( Published: ${publishedAt});
console.error( Age: ${ageInDays.toFixed(1)} days (minimum: ${MIN_AGE_DAYS}));
return false;
}
return true;
}
async function main() {
const lockfile = JSON.parse(readFileSync(LOCKFILE_PATH, 'utf-8'));
const packages = Object.entries(lockfile.packages || {})
.filter(([path]) => path.startsWith('node_modules/'))
.map(([path, info]) => ({
name: path.replace('node_modules/', ''),
version: info.version
}));
console.log(Checking age of ${packages.length} packages...);
const results = await Promise.all(
packages.map(({ name, version }) => checkPackageAge(name, version))
);
const failures = results.filter(r => r === false).length;
if (failures > 0) {
console.error(\n❌ ${failures} package(s) are too new. Aborting.);
process.exit(1);
}
console.log(✅ All packages are at least ${MIN_AGE_DAYS} days old.);
}
main().catch(console.error);
# .github/workflows/security.yml
name: Security Checks
on: [pull_request, push]
jobs:
package-age-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Check package minimum age
run: node scripts/check-package-age.mjs
Package ManagerNative Age SupportScript Support ControlFrozen LockfileAudit Commandnpm❌⚠️ --ignore-scripts✅ --frozen-lockfile✅ npm auditpnpm❌✅ onlyBuiltDependencies✅ --frozen-lockfile✅ pnpm auditYarn Berry❌✅ Constraints✅ --immutable✅ yarn npm auditBun❌✅ ignoreScripts✅ --frozen-lockfile⚠️ ExperimentalDenoN/A✅ Permission model✅ URL pinningN/A
No major package manager supports minimum release age natively — you need to build it yourself
The universal CI script approach works for any package manager and takes under 10 minutes to add
Combine age checking with --frozen-lockfile, disabled install scripts, and audit scanning for defense in depth
Deno's permission model provides an additional runtime layer of protection even if a bad package gets in
Start with a 3-day minimum — it catches the vast majority of "spray and pray" npm attacks with minimal friction