The gap
npm audit runs after the install. By then the package — and any
postinstall script it shipped — is already on your machine. For
supply-chain attacks, "after" is too late.
So I built Vault: an npm-compatible, pnpm-style package manager (written
in Rust) that makes the security check part of install itself.
## What it does before touching node_modules
-
Blocks known CVEs. It audits the resolved graph (OSV + a static scan)
and refuses to install critical/high vulnerabilities.
--forceexists, but you have to mean it. -
No lifecycle scripts by default.
postinstallis the classic malware entrypoint; Vault doesn't run it unless you allow it, and installs run in a Landlock sandbox. - Takeover signals. It warns when a package was published moments ago or when its maintainer count suddenly drops.
## A real block
A project had "vite": "^5.4.11". npm/pnpm happily resolve that to 5.4.21
and install it. That version sits inside the affected range of
GHSA-fx2h-pf6j-xcff (CVE-2026-53571, CVSS 8.2). Vault:
✗ BLOCKED vite@5.4.21: critical/high CVE(s): GHSA-fx2h-pf6j-xcff
error: security policy blocked install
One bump to the patched line and the install went through clean.
## Isn't auditing slow?
It's pnpm-style under the hood (content-addressable store + hard links). In
my tests it lands within ~0.2s of pnpm on a warm cache while running a full
audit every install. It's not faster than pnpm — the point is you don't pay
a real speed tax for the safety.
## Try it / break it
It's early (v0.1.3) and I want the rough edges found.
bash
npm i -g vaultpm # bins: vault / vt
vault install
Repo: https://github.com/Matheusagostinho/vaultpm — issues and harsh feedback
very welcome.
Top comments (4)
Moving audit before install changes the threat model in a good way. Most teams inspect risk after the dependency is already inside the workflow, which is backwards for malicious packages.
The tricky part is avoiding alert fatigue. Blocking on known-dangerous patterns is useful, but the tool also needs clear explanations and escape hatches for cases where a maintainer intentionally accepts the risk.
Exactly — and honestly the alert-fatigue part is the thing I'm least sure I've
gotten right. It's also where most security tooling dies:
npm auditisn'tignored because it's wrong, it's ignored because it cries wolf, so people
blanket-override and the signal goes to zero. A tool that makes "approve"
reflexive again has just rebuilt the exact muscle memory we were trying to
kill.
The design rule I've been converging on is: block on facts, warn on signals,
and never turn a soft signal into a click-through gate. A known critical CVE
is a fact → hard stop. "Published 2 hours ago" or "maintainer count dropped"
are signals → they inform, they don't block, because the moment you make them
a blocking modal you've trained the autopilot Y again.
On the escape hatch, the part that matters to me is that "I accept this risk"
shouldn't be a transient keypress — it should be a durable, reviewable
artifact. So instead of a global
--forcethat disables everything forever,the direction is a scoped exception: pinned to that exact package + version +
advisory ID, written to a committed file, so it shows up in a PR diff and a
human signs off. Two nice properties fall out of that: the decision is visible
to the team instead of living in one person's terminal history, and because
it's pinned to the version, it auto-expires — an exception you granted on
1.2.3 does NOT silently cover a malicious 1.2.4. That last bit is the whole
point in the Shai-Hulud context: a blanket bypass is exactly how a worm gets
waved through.
And to your point about explanations: a block is only as good as the next
action it hands you. "BLOCKED" alone is just a different kind of friction —
it has to show the advisory, the severity, which dependency pulled the thing
in, and whether a fixed version exists and what the upgrade is. In the real
case I mentioned it pointed straight at the patched line, so the block took ~10
seconds to resolve instead of becoming a dead end you
--forcepast.Still very much an open problem though — if you've seen an exceptions/override
UX that actually stayed low-noise over time, I'd genuinely like to copy it.
That scoped exception shape is exactly the part that makes the override safe enough to exist.
The two details I would keep strict are version pinning and reason capture. If the exception is not tied to package + version + advisory, it slowly turns into a permanent hole. If it does not require a short reason, review later becomes archaeology.
The low-noise version I have liked is: hard block on facts, soft surface on risk signals, and every override has an owner plus an expiry condition. That keeps "approve" from becoming muscle memory again.
Thanks for the thoughtful feedback.
I completely agree that version pinning and reason tracking are critical. In Vault, my goal was to make exceptions as explicit and auditable as possible instead of creating a generic bypass mechanism.
I also like the idea of tying approvals to a specific package, version, and advisory, with a clear justification attached. Adding ownership and an expiration policy would make the workflow even stronger and prevent exceptions from silently becoming permanent.
The balance I’m aiming for is exactly what you described: hard enforcement for confirmed security issues while keeping risk signals visible without overwhelming developers with noise.