# 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.*

- Canonical URL: https://www.refolk.ai/guides/vetting-a-new-dependency
- Pillar: Engineering and open source
- Format: Playbook
- Published: 2026-09-08
- Last reviewed: 2026-09-08
- Reading time: 16 min

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.

**~4 hours - How long poisoned axios versions stayed live before npm pulled them**

During that window a CVE feed shows nothing, so metadata signals are all a consumer has.

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.

> **Watch out:** New-and-unpopular is not proof of malice
>
> A package published yesterday with zero downloads trips age and popularity checks, but every legitimate new package starts there. Resolve it with maintainer and repo provenance, never counts alone.

## 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

1. **Normalize** - Apply Unicode normalization and confusable mapping to the candidate name
2. **Measure** - Damerau-Levenshtein against the top packages
3. **Pair** - Combine distance with download count and publish age
4. **Flag** - Distance 0-1 after normalization, or 2 with low downloads

*Normalize before you measure, or a homoglyph spoof scores the same as a harmless typo.*

## 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.

I ran this search: `Application security engineers in the US who list software supply chain security and have spoken at a security conference.` - [see the full result list](https://www.refolk.ai/s/3kgw1yg4g3).

*Returns named practitioners with the exact review skill this procedure depends on, ranked and contactable.*

## 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

1. **Confirm existence and read metadata** - Run npm view to confirm the package exists and capture name, version, license, maintainers, and repo. Defends against AI-hallucinated names.
2. **Name-distance check** - Normalize for Unicode confusables, then compute Damerau-Levenshtein against top packages. Distance 2 or less to a popular name plus low downloads is a flag.
3. **Publish-age and popularity cross-check** - Record age, download count, and dependents. Recent publish plus near-zero dependents plus a near-popular name is the classic malware profile.
4. **Maintainer and ownership check** - Review maintainer history for a recent ownership transfer or a single unknown maintainer, and confirm the repo is real.
5. **Install-script inspection** - Read every preinstall, install, and postinstall hook, then check the tarball root for binding.gyp because a native build triggers with no scripts field.
6. **Automated pre-install gate** - Run npq or guarddog npm scan before any code executes, and read the output rather than auto-approving.
7. **Sandbox-and-test if uncertain** - Install with --ignore-scripts in a no-egress container and observe behaviour; a script that tries to phone home fails.
8. **Lock and enforce** - Commit the lockfile, make CI use npm ci to verify integrity hashes, and configure a cooldown.
9. **Record the verdict** - Reach 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=true` in `.npmrc` to make it the default.
- **Enforce the lockfile in CI.** `npm ci` installs 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.

**Minimal sandbox install (adapt paths and image to your stack)**

```
# .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.

> **Rule:** Cooldown is one layer, never the wall
>
> A 24-hour delay only stops attacks that get yanked in hours. Sophisticated attackers can wait it out, so pair every cooldown with a scanner and manual script review, and never treat the delay as safety on its own.

## 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 ci` matched it, reported a clean install, and executed the RAT. Review lockfile-change PRs for new postinstall scripts rather than trusting the match.
- **`--ignore-scripts` silently 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 to `require('sharp')`. Check with `npm rebuild <trusted-native-dep>` after install.
- **Script-free package still runs code.** A `binding.gyp` alone triggers node-gyp. "package.json has no scripts, so it's inert" is false; check the tarball for `binding.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

Horizontal axis runs from Weak signals to Strong signals. Vertical axis runs from No install-time code to Runs install-time code.

| Quadrant | What it means |
| --- | --- |
| Clean pure-JS, low risk | Install after lockfile pin |
| Signals but inert on install | Reject or sandbox before use |
| Runs code, looks legitimate | Sandbox-and-test, then approve the script |
| Runs code and looks malicious | Reject |

*Two axes - how strong the malware signals are, and whether the install runs code - place a package in a verdict.*

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.

**2.45x - US profiles listing supply-chain security vs software composition analysis skills**

In Refolk's index, 103 list "Software Supply Chain Security" against 42 listing "Software Composition Analysis."

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.

## Frequently asked questions

### 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.

---

*From the Refolk guide library. I revise these guides rather than replacing them, so the current version is always at https://www.refolk.ai/guides/vetting-a-new-dependency*
