DEV Community

CinfiniteDev
CinfiniteDev

Posted on

From pnpm's Cool Feature to npm's Life jacket: The (somewhat accidental) birth of age-install

From pnpm's Cool Feature to npm's Life jacket: The (somewhat accidental) birth of age-install

Or: How I built a tool nobody asked for, everyone needs, and should've made years ago


It started with a blog post (as these things do)

I was procrastinating—er, researching—when I stumbled across pnpm's release notes for version 10.16. The headline practically screamed at me:

New setting for delayed dependency updates

"Oh cool," I thought. "They added a feature."

Then I actually read the description and my brain did a little somersault.

See, supply chain attacks on npm packages have been having a moment lately. Every other week, someone's favorite utility package gets hijacked, malicious code sneaks in, and suddenly your CI server is mining cryptocurrency for some script kiddie in some place. (No offense to script kiddies everywhere. I'm sure you're very talented.)

The beautiful part? These attacks get caught fast. Like, really fast. Usually within an hour, the malicious version is gone, the maintainer is panicking on Twitter, and everyone's scrambling to update their lockfiles.

So here's the genius part: pnpm thought, "What if we just... didn't install fresh packages? Like, at all?"

Mind blown emoji


The itch (that classic developer problem)

I moved on with my day. Fixed some bugs. Went to meetings about meetings. The usual. (NO not at all , this is not a typical day for me anymore. I mean if it was really a day like that , you and i wont be having such intellectual conversations.)

But the idea wouldn't leave. Because I opened my terminal and typed:

npx pnpm install react
Enter fullscreen mode Exit fullscreen mode

My fingers betrayed me. Muscle memory won. I was in an npm project, and switching package managers for one feature felt like overkill.

So I did what any reasonable developer does when they have an itch: I told myself I'd think about it tomorrow, then opened VS Code at 11 PM and started typing anyway.


Building the prototype (and discovering that everything is hard)

The concept was stupidly simple:

  1. Take a package name
  2. Ask npm: "Hey, when was this thing published?"
  3. Do math: now - published = too fresh?
  4. If too fresh: "Sorry, come back later"
  5. If old enough: proceed with install

How hard can it be? Famous last words.

Turns out: pretty pretty .

Version ranges are chaos

Someone types npm install react@^18.0.0. What version are we actually talking about?

The registry might return 18.0.0. Or 18.1.0. Or 18.2.0. Or whatever the latest in that range is. So now we have to:

  1. Resolve the semver range
  2. Get the actual version number
  3. Then check the timestamp

Great. So much for "simple."

Scoped packages will break your heart

Regex, right? Find the @, split on it, done.

Except @babel/core has an @ symbol in the middle. My first implementation thought the package was @babel and the version was core. npm's error message was less helpful than my code.

Lesson learned: when parsing scoped packages, you need to find the second @. (Who designed this syntax?)

API rate limits are real

Every package check = one npm registry call. And npm will rate limit you if you're too aggressive.

So: cache everything. Timestamps never change anyway. Once you know when lodash@4.18.1 was published, you know it forever. Just write it down.

Trust issues (the list kind)

Some packages always need the latest version. Webpack? Trust webpack. Babel? Trust babel. @types/*? Yeah, probably fine.

So exclusions had to happen. Because forcing everyone to wait for every package is how you get developers who disable the safety feature entirely.


The feature nobody asked for (but everyone desperately needs)

I showed a working prototype to a friend. Their reaction:

"Wait, this isn't built into npm? I thought this was a basic security feature by now."

And honestly? That hurt. Because they were right. This should be a default. The fact that it's not feels like a plot hole in npm's security story.

But also—validation. If a smart person like them assumed it existed, maybe other smart people did too. Maybe there was an audience for this.

So I kept building.


What is age-install? (for the uninitiated)

age-install is a CLI tool that makes npm wait before installing fresh packages. Think of it as a bouncer for your node_modules.

# Before age-install
npm install react  # installs immediately, even if published 5 minutes ago

# After age-install
age-install install react  # checks timestamp first, blocks if too new
Enter fullscreen mode Exit fullscreen mode

The default minimum age is 1440 minutes (24 hours). You can configure it to whatever paranoia level suits you.

# Default: 1440 minutes (24 hours)
age-install install react lodash

# Paranoid: 2 days
age-install install express --minimum-age 2880

# "I trust webpack but nobody else"
age-install install webpack @babel/core --exclude webpack
Enter fullscreen mode Exit fullscreen mode

The check command (for the indecisive)

Ever wanted to see what's safe before committing? That's check:

age-install check react lodash express
Enter fullscreen mode Exit fullscreen mode

Output:

📋 Checking 3 package(s)...

✅ Safe to install (old enough):
   - react@19.2.6 (207.8 hours old)
   - lodash@4.18.1 (1043.1 hours old)

⚠️  Too new (would be blocked):
   - express@5.0.0 (15 minutes old, min: 60 min)

⏭️  Excluded (no checks performed):
   - webpack

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 Summary: 2 safe, 1 blocked, 1 excluded
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Enter fullscreen mode Exit fullscreen mode

Perfect for: CI/CD pipelines, pre-commit hooks, paranoid Fridays, impressing your security team.


The JSON report (because engineers love data)

Sometimes you need to prove to someone else that you checked. Or save it for audit purposes. Or feed it into your security dashboard.

age-install check --report
Enter fullscreen mode Exit fullscreen mode

Generates something like:

{
  "generated": "2026-05-15T08:30:00.000Z",
  "minimumAge": 60,
  "summary": {
    "safe": 2,
    "blocked": 1,
    "excluded": 0,
    "total": 3
  },
  "safe": [...],
  "blocked": [...],
  "excluded": [...]
}
Enter fullscreen mode Exit fullscreen mode

Now you can email this to your manager, attach it to a Jira ticket, or programmatically fail your CI if anyone's feeling spicy.


Configuration options (because one size fits nobody)

Set it once, forget about it:

package.json:

{
  "ageInstall": {
    "minimumReleaseAge": 60,
    "minimumReleaseAgeExclude": ["webpack", "^@babel/", "@types/*"]
  }
}
Enter fullscreen mode Exit fullscreen mode

.npmrc:

age-install.minimumReleaseAge=60
age-install.minimumReleaseAgeExclude=webpack,vite
Enter fullscreen mode Exit fullscreen mode

Environment:

AGE_INSTALL_MIN_AGE=60
AGE_INSTALL_EXCLUDE=webpack,vite
Enter fullscreen mode Exit fullscreen mode

Priority order: CLI flags > environment > config files > defaults.


The philosophy (getting deep for a second)

Here's the thing about supply chain attacks: most of them have the shelf life of a banana.

The malicious version goes up. Someone notices. The maintainer gets alerted. The version gets yanked. Usually within hours , sometimes an hour too.

The security here isn't in detecting the attack—it's in waiting it out.

age-install doesn't protect against sophisticated, patient attackers. It protects against the opportunistic ones—the script kiddies, the crypto-farmers, the folks who publish and pray.

For that threat model? Waiting an hour / a day , whatever works for you, is basically free insurance.


How it works (for the technically curious)

You: age-install install express@5

1. Parse: "express@5" → name="express", version="5"
2. Resolve: "5" → "5.0.0" (query npm registry)
3. Exclusion check: Is "express" in the exclusion list? No.
4. Cache check: Do we have this timestamp? No.
5. Registry query: "npm view express@5.0.0 time" → "2026-05-15T08:15:00Z"
6. Calculate: Now (08:30) - Then (08:15) = 15 minutes
7. Decision: 15 < 60. TOO NEW. BLOCKED.
8. Cache: Save timestamp for next time.
Enter fullscreen mode Exit fullscreen mode

No machine learning. No threat feeds. Just timestamps and patience.


Why npm doesn't have this (my conspiracy theory)

I've thought about this a lot. Here are my theories:

1. Performance obsession
npm is obsessed with speed. Every millisecond counts. Adding an API call per package would slow things down—probably noticeably.

2. "Just install" philosophy
npm's whole vibe is frictionless. Type npm install. Get packages. Done. Adding friction—even security friction—goes against the brand.

3. Complexity creep
Version ranges, exclusions, scoped packages, caching... what sounds simple in a blog post is actually a decent amount of state to manage.

4. Nobody filed a ticket (probably)
Maybe this just never made it onto npm's roadmap. Bug them about it. Tell them age-install exists and they should steal the idea.


The future (where dreams are made)

Right now, age-install only checks packages you explicitly install. It doesn't scan transitive dependencies.

In an ideal world, we'd catch malicious packages before they enter your node_modules. But that requires parsing the entire dependency tree first—expensive and complex.

Maybe future versions. For now, direct dependency checking is better than nothing. And nothing was the alternative.


Get started (before it's too late)

npm install -g age-install

# Quick test
age-install --version

# Actually use it
npx age-install install react lodash
Enter fullscreen mode Exit fullscreen mode

Or just peek at what would happen:

age-install check react lodash express
Enter fullscreen mode Exit fullscreen mode

Link for reference :

https://www.npmjs.com/package/age-install

Enter fullscreen mode Exit fullscreen mode

The end (but also the beginning)

I never expected building age-install to be this much learning and fun . What started as "oh that'd be cool" became a weekend project, then a serious tool, then an article you're reading right now.

Will anyone else use it? Maybe. Maybe not. But if it saves even one project from running malicious code, I'll consider it a win.

And if not? Well—I'll at least understand minimumReleaseAge better than I did yesterday. That's worth something, right?


Built by cinfinit with VS Code, zeroooo caffeine, and a healthy distrust of packages published in the last hours.

P.S. - If you found this useful, tell your friends. If you found it useless, tell your enemies. Either way, we're even.

Top comments (0)