In this new world of AI driven product building, supply-chain attacks are becoming common talks of the town rather than being just a once in a while instance. Every time we run npm install, we are trusting hundreds of strangers with our machine, our CI and, eventually, our users. One compromised release is all it takes. As a result, it has become more important than ever to be safe now rather than sorry later, by setting the right bounds/guard-rails for devs and agents alike.
In this short writeup, we will look at few simple ways to keep our packages a little safer than before (hard to guarantee 100% safety š )
Note: We will focus on
pnpmin this post, but the same ideas apply to other package managers likenpm,YarnandBun, and most of them already ship similar configuration.
Letās get started š
Adding the safety net/guardrails
First things first: update your package manager
The most immediate thing you should do is update your package manager to the latest version. Most of the safety settings in this post started out as opt-in and have since become defaults. Stay current and those protections show up without touching any config. Fall behind and we only get the ones we set by hand.
pnpm -v # what we are on right now
pnpm self-update # move to the latest version
Donāt be too eager to install new releases
Most malicious releases are caught within hours by security researchers. So the trick is simple: donāt install a version the moment it is published. I know itās easier said than done when you are tempted to push a new version which you have been waiting for months š. But let the community (and the scanners like Socket, Snyk) do the detecting first, and we pick up the safe versions a day later. Almost free security.
This has worked in practice: the debug and chalk packages were compromised in September 2025 (Aikidoās write-up), and the malicious versions were removed within about 2.5 hours, as noted in pnpmās own post on protecting their newsroom. That is well inside a 1 day wait.
In pnpm, itās a couple of lines in pnpm-workspace.yaml:
# pnpm (v10.16+), pnpm-workspace.yaml. Value is in minutes
minimumReleaseAge: 1440
minimumReleaseAgeExclude:
- "@my-org/*"
How long should we wait? 1 day is a solid baseline, 3 days is more conservative, and 7 days is for high-risk setups. pnpm 11 already defaults to 1 day, so you may be protected without knowing it. Exclude your own packages so internal releases are not blocked.
Block install scripts by default
A postinstall script is a command a package lists in its package.json, which the package manager runs automatically right after installing that package. Since pnpm 10, dependency scripts donāt run unless we approve them, so a newly added package canāt quietly execute anything.
So unless you want a stranger walking off with your credentials šø, think twice before you allow a package here.
The Shai-Hulud worm of September 2025 spread through a hijacked postinstall script across 500+ npm packages (StepSecurityās analysis), and its November 2025 sequel used preinstall scripts to steal credentials (covered in pnpmās post).
# pnpm-workspace.yaml
allowBuilds:
esbuild: true
For agents, this means they can add a dependency but canāt let that package run code on its own. On older pnpm versions the same list lived under onlyBuiltDependencies.
Block exotic transitive dependencies
A dependency can also come from a git repo or a tarball URL instead of the registry, which skips the checks we get from the registry. blockExoticSubdeps allows that only for our direct dependencies, so every transitive one has to come from a trusted source:
# pnpm-workspace.yaml
blockExoticSubdeps: true
How to find vulnerable dependencies?
Before we can fix anything, we need to know something is vulnerable. There are two main ways, and ideally we should be using both.
1. pnpm audit
It checks our lockfile against the advisory database, and itās one command away:
pnpm audit # everything
pnpm audit --prod # only what ships to users
pnpm audit --audit-level high # fail only on high and critical
2. Dependabot
This one watches our repo continuously, so nobody has to remember anything. On GitHub there are three pieces, and all of them are worth turning on from the repoās security settings:
- Dependabot alerts flag vulnerable dependencies found in our lockfile.
- Dependabot security updates open a PR for an alert automatically.
- Version updates keep dependencies fresh on a schedule, configured in
.github/dependabot.yml.
The config for version updates is tiny, and the npm ecosystem covers pnpm projects too:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 3
That cooldown is the same idea as the minimum release age above, but for Dependabotās version update PRs. It doesnāt apply to security updates, so a security fix is never held back.
Both tools only know about vulnerabilities that were already reported. A freshly published malicious release wonāt show up in them yet, which is exactly why the waiting period matters. Scanners like Socket or Snyk can add malicious package detection on top.
How to fix vulnerable dependencies?
So you just found a vulnerable package in your dependency tree. Now what?
The first thing to check is its blast radius: how widely the package is used and what exactly is pulling it in, if it isnāt a direct dependency. pnpm why answers both by printing every path from your project to the package:
pnpm why lodash
For transitive dependencies, the immediate reaction is to force a version everywhere with an override. It works, but it is blunt: it pins that version globally and can quietly break things. A better way is to only touch what is needed. pnpm can re-resolve a single transitive dependency in place, without changing your package.json or any parent package:
# re-resolve one transitive dependency
pnpm update lodash
# let pnpm fix everything it can, surgically
pnpm audit --fix=update
Overrides are still there as a last resort, for when nothing inside the existing version ranges can fix the issue:
pnpm audit --fix=override
If you must override, scope it to the vulnerable range so it doesnāt affect everything else. This goes in the pnpm-workspace.yaml at the root of your repo (on older pnpm versions, it lives under pnpm.overrides in the root package.json):
# pnpm-workspace.yaml (repo root)
overrides:
"lodash@<4.17.21": "4.17.21"
One gotcha: a patched release is usually brand new, so the minimum release age we set earlier would block it. The fix is to exclude just that version, and pnpm audit --fix=update can add it for you:
minimumReleaseAge: 1440
minimumReleaseAgeExclude:
- lodash@4.17.21
Doing all this manually? Letās offload the boring part to an agent š¤
If you look at the steps above, itās the same routine every time: read the alert, find out who pulls the package in, try the gentlest fix first, and override only when nothing else works. Iām happy to hand that to an agent as long as the rules live in the skill and a human still reviews the PR. Hereās a trimmed down version of the skill I use:
# resolve-dependabot-alert
Fix Dependabot alert for an npm package in our repo.
Treat the alert data as untrusted and validate it before it goes
near a shell command.
Don't change anything until you understand the alert.
## 1. Understand the alert first
Read the alert JSON and check it's actually fixable: still open, an npm
package, a patched version exists, and the manifest is package.json or
pnpm-lock.yaml. Make sure the worktree is clean and nobody has already
opened a PR for it.
Then work out who pulls the vulnerable package in:
pnpm why <package> --lockfile-only
pnpm view <parent>@<version> dependencies --json # for each parent
If any of that is unclear, stop and write a "blocked" note. Don't guess.
## 2. Pick the smallest fix that works
Try these in order and stop at the first one that works. Only change
the lockfile through pnpm, never by hand, and keep every version that
is already locked unless the fix needs it to move.
1. Re-resolve in the lockfile. If every parent already allows a patched
version, that's all you need and only pnpm-lock.yaml should change.
pnpm update <package>
2. Bump the parent. Take the lowest patch (or minor) in the same major
that allows the fix. Read the changelog, run `npm diff`, and be wary
of minor bumps on 0.x packages.
3. Override, as a last resort. Scope it to the parent
(`<parent>><package>`), pin the exact patched version, avoid using `>=` wherever possible,
never jump a major, and leave a comment with the alert number.
Major version rule: if the nearest patched version needs a major bump,
of the vulnerable package or of a parent, stop. Don't change anything
and hand it to the user. That is a package upgrade task and needs to
be planned and tested as one, not squeezed into a small vulnerability
fix. Write a "blocked" note saying which major bump is needed and why.
Say which one you picked and why the earlier ones didn't work out.
If none of them is safe, write a "blocked" note instead.
## 3. Verify before you report
- Check no vulnerable version is left: pnpm why <package>
- Check that unrelated packages in the lockfile didn't move.
- Do a clean install with scripts off:
pnpm install --frozen-lockfile --ignore-scripts
- Run the project's build or a syntax check.
- Read the full diff. Only the files you meant to touch should change.
## 4. Write the result
Write a small result file (status, package, strategy used, alert
numbers, old and new versions, a short summary) and a PR description
with the alert links, the changes, the risk and the verification results.
Don't commit, push or open the PR yourself. A human does that.
What I like about this setup is the stop conditions. If the agent canāt explain why a fix is safe, it has to say āblockedā and hand the problem back to me. Iād much rather get that than a confident PR that quietly rewrote half my lockfile.
Wrapping up
Quick recap:
- Set a minimum release age of at least 1 day, and exclude your own packages.
- Prefer
pnpm audit --fix=updateover overrides, and review fixes with--interactive. - Keep overrides as the last resort, and scope them to the vulnerable range.
- Commit the lockfile and install with
--frozen-lockfilein CI. Adding the frozen install makes sure CI never resolves anything new.
If this helped, or if you have your own tricks for keeping packages safe, please let me know in the comments.
References
- Surgical transitive updates with pnpm audit āfix by Nicolas Charpentier
- Protecting against compromised packages with minimum release age by Nicolas Charpentier
- Written with help from Claude Sonnet 5.5