Vetting a New Dependency Before It Runs on Your Machine
You can take an unfamiliar package name and reach a defensible install, sandbox-and-test, or reject decision without waiting for a CVE.
You are about to add a package you have never used before, and the install command is going to run its code on your laptop or in CI. This guide is the consumer-side screen for that moment: an ordered sequence of registry, maintainer, and install-script checks that ends in a defensible install, sandbox-and-test, or reject decision. It is written for engineers, technical founders, and sourcers who need a repeatable method that does not wait for a CVE, because for a brand-new malicious package there is no CVE yet.
This is not incident response for a named advisory, and it is not a scan for known vulnerabilities. It is a first-adoption malware screen built around four things a CVE database cannot tell you in time: how old the version is, how close its name sits to a popular one, whether the maintainer looks anomalous, and what the install scripts actually do.
Why a CVE scanner cannot help you here
The whole method exists because first-adoption malware leaves no advisory during the window you care about. A poisoned version is published, run by early adopters, detected, and yanked within hours - and only afterward does an advisory appear, too late for the machines that already installed it.
The numbers make the timing concrete. When malicious axios versions were published, they were live for about four hours before npm pulled them, and axios sees roughly 100 million weekly downloads with a payload that executed in about two seconds on install. A CVE-based scanner querying a vulnerability feed sees nothing during those four hours, because the entry does not exist yet. Other campaigns give you more room but not much: the MUT-4831 Vidar packages stayed live around two weeks and still pulled at least 2,240 downloads before removal.
So the input to this procedure is not a scanner's verdict. It is the registry metadata, the maintainer record, and the code that runs at install time. Everything below is built to read those directly.
The signals that actually separate safe from malicious
The reliable signals are publish age, download count against claimed popularity, whether a real repository exists, and what the install scripts do. None is individually dispositive; their combination is the detection surface.
OWASP's NPM cheat sheet is blunt about the baseline: run npm view before installing to confirm the package exists, check the download count (legitimate packages have thousands or millions while newly published malicious ones have very few), and verify a real GitHub repository. Beyond that, install-script presence is itself a risk signal because legitimate packages rarely need a postinstall, and version anomalies - jumping from 1.0.4 to 99.0.0 - plus a recent publish with zero dependents are classic markers. Packages that recently transferred ownership, especially to an organization you have never heard of, deserve extra scrutiny.
The concrete metadata set is small and worth memorizing. The npq tool's marshalls check exactly this: package age on npm, download count as a popularity metric, presence of a README, presence of a LICENSE, and pre/post-install scripts.
| Signal | What it proves | What it looks like when it lies |
|---|---|---|
| Publish age | Fresh versions have had no detection time | Every legitimate new package is also brand new |
| Downloads vs popularity | Malware rarely has real adoption | A new honest package also starts at zero |
| Install-script presence | Code runs before you import anything | sharp, prisma, puppeteer all have honest scripts |
| Name distance to a popular pkg | Typosquats sit within 1-2 edits | A homoglyph and an innocent typo share a distance |
| Real repo and maintainer | Provenance is checkable | A repo link can point at code that is not shipped |
One caution on scope: a maintainer email-domain mismatch is sometimes cited as a signal, but it is not established in the public sources I trust here. Treat it as a soft prompt to look harder, not as evidence.
The name-distance check, done right
Typosquat detection compares the candidate name against popular package names using edit-distance, but you have to normalize for Unicode confusables before you measure, or homoglyphs slip through. The rule of thumb: normalize, then compute Damerau-Levenshtein, then flag close matches paired with low adoption.
The distance threshold is empirical. In one historical set, 18 of 40 typosquats sat within a Levenshtein distance of 2 or less from their targets, meaning one or two edits was enough. A common reference tool sets its default tolerated distance to 2 against the top 10,000 packages. So distance 2 or less to a popular name, combined with few downloads and a recent publish, is your flag.
The trap is homoglyphs. A homoglyph spoof and an innocent typo can have the exact same edit distance, so plain Levenshtein cannot separate them. The fix is a normalization step first: apply Unicode normalization and confusable mapping, then compute the distance against the normalized string, and flag matches at distance 0 or 1 after normalization. Distance 0 after normalization is especially damning - it means the name is visually identical to a real one but encoded with different characters.
Name-distance screen
- NormalizeApply Unicode normalization and confusable mapping to the candidate name
- MeasureDamerau-Levenshtein against the top packages
- PairCombine distance with download count and publish age
- FlagDistance 0-1 after normalization, or 2 with low downloads
The install-script problem, and its honest exceptions
Install scripts are where the attack lives and where the false positives cluster, so read them, but do not treat their mere presence as a verdict. A malicious script fetches and executes a remote payload; a legitimate one builds a native addon and has a visible reason to exist.
This matters because npm is the only remaining major package manager that runs dependency install scripts by default. pnpm v10 and later, Yarn Berry, Bun, and Deno all block them. The direction of travel is clear too: npm v12 will block install scripts by default, and 11.16.0 already ships a --strict-allow-scripts mode to make skipped-build failures loud instead of silent.
When you read a postinstall, you are mostly looking for two things. GuardDog's own data is the shortcut here: just two of its 22 rules, npm-install-script and shady-links, account for over 90% of its detections. So a human who reads the install hooks and the outbound URLs they contact covers most of the real attack surface without any tooling at all.
Two rules catch most npm malware, so a reviewer who reads install scripts and outbound URLs is not outgunned.
The honest exceptions are a small, enumerable set. These packages run code at install for a real reason, and blocking them globally breaks your build.
| Package | Reason for install script |
|---|---|
| sharp / canvas | Native addon, prebuilt binary or compile |
| better-sqlite3 / sqlite3 | node-gyp native build |
| bufferutil / utf-8-validate | Optional ws native builds |
| puppeteer / playwright | Download browser binary |
| prisma | Postinstall generates client |
Because this set is small and clusters tightly, per-package approval beats a global block. And there is a subtlety that catches people: a package does not even need a scripts block to trigger a native build. A binding.gyp file in the package root is enough - node-gyp sees it and compiles. So "package.json has no scripts, so it's inert" is a false positive. Check the tarball root for binding.gyp as a distinct step.
The procedure, start to finish
Run these in order. The whole screen takes about 20 minutes for a package you decide to sandbox, and under 10 for a clear install or reject. The ordering below front-loads the cheap metadata reads and puts the sandbox last, only when you are still uncertain.
Vetting a new dependency, in order
- Confirm existence and read metadataRun npm view to confirm the package exists and capture name, version, license, maintainers, and repo. Defends against AI-hallucinated names.
- Name-distance checkNormalize for Unicode confusables, then compute Damerau-Levenshtein against top packages. Distance 2 or less to a popular name plus low downloads is a flag.
- Publish-age and popularity cross-checkRecord age, download count, and dependents. Recent publish plus near-zero dependents plus a near-popular name is the classic malware profile.
- Maintainer and ownership checkReview maintainer history for a recent ownership transfer or a single unknown maintainer, and confirm the repo is real.
- Install-script inspectionRead every preinstall, install, and postinstall hook, then check the tarball root for binding.gyp because a native build triggers with no scripts field.
- Automated pre-install gateRun npq or guarddog npm scan before any code executes, and read the output rather than auto-approving.
- Sandbox-and-test if uncertainInstall with --ignore-scripts in a no-egress container and observe behaviour; a script that tries to phone home fails.
- Lock and enforceCommit the lockfile, make CI use npm ci to verify integrity hashes, and configure a cooldown.
- Record the verdictReach install, sandbox-and-test, or reject, and write down which signals drove the call.
The automated gate at step six is npq or GuardDog. npq intercepts npm, yarn, and pnpm install commands, runs heuristic checks on age, download metrics, maintainer behavior, scripts, provenance, and typosquatting, and either auto-continues or requires manual approval before delegating to the package manager. GuardDog scans the code and metadata statically. Note that sources disagree on ordering - some checklists put the automated gate second - but the metadata-first sequence here means a hallucinated or typosquatted name gets caught before you spend time reading scripts.
Running it in isolation without trusting it
If you reach step seven, install the package with scripts disabled inside a locked-down container that has no network egress except to the registry. That way a postinstall that tries to reach out to an attacker's host fails, visibly, and you see it.
There are four documented isolation layers, and you can combine them:
- Disable scripts. The npm CLI has an ignore-scripts flag that prevents execution of any lifecycle hooks defined in the package.json of packages you install. Set
ignore-scripts=truein.npmrcto make it the default. - Enforce the lockfile in CI.
npm ciinstalls strictly from package-lock.json, errors out if the lockfile is out of sync with package.json, and verifies every package's integrity hash against the lockfile as it goes. - Sandbox the install. Run installs in a locked-down container with no network egress except to the registry, so a script that tries to phone home cannot reach its destination.
- Static-scan before install. Gate the install with npq, and scan code and metadata with GuardDog, before any package code runs.
# .npmrc in the sandbox dir ignore-scripts=true # then, in a no-egress container: npm install <package>@<version> --ignore-scripts --no-save # inspect the unpacked files, grep install hooks and URLs: cat node_modules/<package>/package.json | grep -A2 scripts find node_modules/<package> -name binding.gyp # re-enable a build only for a trusted native dep: npm rebuild <trusted-native-dep>
Run in a throwaway container with egress limited to the registry; observe, do not deploy.
Cooldowns: useful, and not a wall
A cooldown blocks installing a version until a minimum time has passed since it was published. It buys the community time to detect and yank malware before it reaches you, which is exactly why it works against smash-and-grab attacks measured in hours. It does not stop a patient attacker who waits the window out.
The tooling has converged on this, with different defaults and units. Know which one your stack uses, because a misconfigured cooldown either does nothing or blocks installs you actually need.
| Tool | Setting name | Unit | Default |
|---|---|---|---|
| pnpm | minimumReleaseAge | minutes | 1440 (on by default, v11) |
| npm | min-release-age | days | off by default (11.10.0+) |
| Dependabot | cooldown.default-days | days | security updates bypass |
| Renovate | minimumReleaseAge | duration | 3 days (best-practices preset) |
| Snyk | (built-in) | days | 21 (non-configurable) |
pnpm defaults minimumReleaseAge to 1440 minutes, one day, and 10080 is a week; since pnpm 11 it is on by default, making pnpm the first major package manager to ship a 24-hour cooldown out of the box. npm followed with a native min-release-age config in 11.10.0, off by default. Dependabot shipped cooldown in July 2025, with security updates bypassing it. Snyk takes the most aggressive stance with a non-configurable 21-day cooldown on automatic upgrade PRs, and Renovate's config:best-practices preset waits three days for npm updates.
How this goes wrong
The failure modes here are mostly false comfort: signals that feel like proof but are not, and defenses that appear to have run when they were quietly skipped. Give each one a countermeasure.
- Cooldown as false comfort. A delay only stops smash-and-grab malware that gets yanked in hours. Check by pairing cooldown with a scanner, not relying on it alone.
- Lockfile integrity is not safety. A pinned SHA-512 verifies the bytes match what the registry published, even when those bytes are malicious. The axios case proves it: the registry computed the correct hash for the poisoned version, a later
npm cimatched it, reported a clean install, and executed the RAT. Review lockfile-change PRs for new postinstall scripts rather than trusting the match. --ignore-scriptssilently breaks native modules. npm still completes the install, it just skips the build. You get no error at install time and then a runtime error when code tries torequire('sharp'). Check withnpm rebuild <trusted-native-dep>after install.- Script-free package still runs code. A
binding.gypalone triggers node-gyp. "package.json has no scripts, so it's inert" is false; check the tarball forbinding.gyp. - Edit-distance misses homoglyphs. A homoglyph spoof and an innocent typo can have the exact same edit distance. Normalize Unicode before comparing.
- New-and-unpopular is not malice. Every legitimate new package trips the age and popularity checks. Resolve via maintainer and repo provenance, not counts.
- npq is interactive, not a CI gate. It relies on registry metadata, not independent key management, so clean-metadata malware with no CVE can still pass. Use it as one layer among several.
- Cooldown drift. If Renovate or Dependabot cooldown is not configured independently, they keep opening PRs you cannot actually install.
Where a package lands after the screen
The Shai-Hulud worm in September 2025 shows why the code-execution axis matters most: a self-replicating postinstall payload compromised 500-plus npm packages by stealing maintainer tokens. Blocking install-time execution, or watching it in a sandbox, is the single defense that would have contained it.
Before you call the job done
Run this checklist against your decision. If any item fails, you are not finished - you are guessing.
Pre-verdict checklist
- I confirmed the package exists with npm view and recorded name, version, license, maintainers, and repo.
- I normalized the name and know its edit distance to the nearest popular package.
- I recorded publish age, download count, and dependent count.
- I reviewed maintainer history for ownership transfer or a lone unknown maintainer.
- I read every preinstall, install, and postinstall hook and their outbound URLs.
- I checked the tarball root for a binding.gyp even though there was no scripts field.
- I ran npq or guarddog npm scan before any package code executed.
- If uncertain, I installed with --ignore-scripts in a no-egress container and observed behaviour.
- The lockfile is committed and CI uses npm ci to verify integrity hashes.
- I wrote down the verdict and the signals that drove it.
Keeping the screen current
The mechanics shift under you, so re-check three things periodically rather than trusting a snapshot. First, default behavior: npm v12 will block install scripts by default, which changes what your global .npmrc needs to say. Second, cooldown units and defaults, since pnpm measures minutes and npm measures days and their defaults differ. Third, your automated gate's rule set, because a scanner's coverage is only as good as its current rules.
There is a staffing reality behind all of this. Manual dependency review is a scarce skill. In Refolk's index there are 1,125 Application/Product Security Engineers in the United States against just 50 in Germany, a 22.5-to-1 gap, so most teams outside a few US hubs cannot staff human review at all and must lean on the ordered checklist and automated gates above.
The field itself frames the problem as process and provenance rather than scanner output: in Refolk's index, supply-chain specialists outnumber pure composition-analysis specialists 2.45 to 1, which matches the cooldown-and-review direction of every recent primary source. When you do need to hire or borrow this skill, Refolk lets you name the exact profile - an AppSec engineer who lists supply-chain security and has spoken at a conference, or an engineer who has contributed to GuardDog, Socket, or npq - and get named, contactable people back. That is the human gate this procedure keeps pointing to, and it is the one part a checklist cannot replace.
Questions practitioners ask
How do I vet an npm package before installing it if there's no CVE yet?
Screen the registry metadata, not a vulnerability database. Confirm the package exists with npm view, measure the name's edit distance to popular packages, cross-check publish age against download count and dependents, review maintainer history, and read every install script plus check for a binding.gyp. First-adoption malware is yanked within hours, so no advisory exists during your exposure window and metadata signals are all you have.
Does disabling install scripts make a package safe?
It stops install-time code execution, which catches most npm malware, but it silently breaks packages with legitimate native builds like sharp, better-sqlite3, and puppeteer. npm still completes the install and skips the build, so you get a runtime error later when code tries to require the module. Prefer per-package approval over a global ignore-scripts, and run npm rebuild on trusted native deps to confirm they still work.
Can a package run code at install without any postinstall script?
Yes. A binding.gyp file in the package root is enough on its own: node-gyp sees it and compiles, with no scripts field in package.json. That is why checking the tarball root for binding.gyp is a separate step from reading the scripts, and why 'no scripts block means inert' is a false positive worth guarding against.
Is a pinned lockfile with integrity hashes enough protection?
No. The SHA-512 integrity in package-lock.json is a hash of the tarball the registry served, so it certifies provenance, not safety. When malicious axios was published, the registry computed the correct hash, a later npm ci matched it, reported a clean install, and executed the payload. Pin your dependencies, but review lockfile-change PRs for new postinstall scripts rather than treating a match as a clean bill of health.
How do I run a typosquatting check before install?
Normalize the candidate name with Unicode confusable mapping first, then compute Damerau-Levenshtein distance against the top packages. Flag matches at distance 0 or 1 after normalization, and treat distance 2 or less as suspicious when paired with low downloads. Edit distance alone cannot separate a homoglyph spoof from an innocent typo, which is why the normalization step comes first.
What is a package install cooldown and does it stop attacks?
A cooldown blocks installing a version until a minimum time has passed since publish. pnpm defaults minimumReleaseAge to 1440 minutes, npm exposes min-release-age in days, and Snyk enforces a non-configurable 21-day cooldown. It stops smash-and-grab attacks that get yanked within hours, but a patient attacker can wait it out, so pair the cooldown with a scanner and manual review rather than relying on it alone.
Try it on the search you came here for
Stop building boolean strings. Just describe the person.
Type one sentence. I plan the search, read GitHub, public LinkedIn and Crunchbase records, and the open web as it is right now, and hand back a ranked list with the reason next to every name.
01Describe them
One plain sentence. Role, city, stack, stage, whatever matters to you.
02I read the web live
GitHub, public LinkedIn and Crunchbase records, the open web. Not a database that went stale last quarter.
03You read the shortlist
Ranked, with the reasoning under every name. Open a profile, ask a follow-up, narrow it down.
- Staff backend engineers in NYC who shipped Rust in production
- Series A fintechs in SF under 50 people, growing headcount this year
- Maintainers of fast-growing Rust web frameworks on GitHub
- No boolean, no filters, no seat to buy. One box.
- Read at search time, so a profile updated yesterday counts today.
- Every step visible as it runs, every name with its reason.
500 free credits on sign-up. No card, no demo call. See real searches.