DepWall

Home / Blog

  • npm
  • how-to
  • supply chain

How to check if an npm package is safe before you install it

Seven checks you can run in under a minute, what each one actually proves, and — the part most guides skip — the specific attacks that pass all seven.

Most guides to this question give you a list of commands and stop. The commands are fine. What is usually missing is the second half: what each check proves, and what it leaves open. A check you trust past its evidence is worse than no check, because you stop looking.

So: seven checks, in the order that costs least, each with its limit stated. Then the attacks that survive all of them.

1. Confirm the name is the name you meant#

This is the highest-yield check and it takes three seconds.

npm view eslint-plugin-unused-imports

Typosquatting substitutes a character. Slopsquatting substitutes a plausible package that never existed — an AI suggests it, an attacker has already registered it. The name unused-imports is the canonical example: models consistently suggest it instead of the real eslint-plugin-unused-imports, somebody registered it, and as of February 2026 it was still recording roughly 233 weekly downloads while security-held.

If a model or a blog post handed you the name, verify it against the project's own documentation rather than against the registry. A name existing in the registry is not evidence it is the right one — pre-registration is the entire attack.

What it proves: the package exists. What it misses: whether it is the one you wanted. The registry will happily confirm the attacker's package.

2. Read the age and the download count together#

npm view lodash time.created time.modified
npm view lodash dist-tags

Neither number means much alone. A package three days old with 40 downloads is unremarkable if it is genuinely new, and a strong signal if it carries the name of something established. High downloads mean visibility, not safety — a popular package is a better target, not a safer one.

The combination that should stop you: recently published, low adoption, named like something you already trust.

What it proves: the package has a history. What it misses: a compromised release of a mature package. Age and downloads are attributes of the name, and the attack ships in a version.

3. Look for install scripts before anything executes#

This is where the malware is. In one benchmark analysis of 6,420 malicious packages, 4,636 — 72.21% — used installation scripts as the attack vector, with preinstall alone appearing in 61.15%. Notably, 1,362 of them (21.9%) had no malicious JavaScript in any file at all: the entire payload was the lifecycle hook. JFrog's figure over a comparable window is lower but the same shape — 46%.

npm view some-package scripts

If you want to fetch the code without running it:

npm install some-package --ignore-scripts

In CI, make that the default rather than the exception:

npm ci --ignore-scripts

The catch is that legitimate packages use the same hooks. husky, patch-package and prisma all need lifecycle scripts to work. "Has a postinstall" is not a verdict — it is a reason to read the script.

What it proves: whether code runs at install time. What it misses: payloads that wait for require() instead. Disabling scripts does not make a hostile package safe to import.

4. Check that the repository is real#

Open the repository field and look at it. Not whether it is populated — whether it is real: commits spread over time, issues with other humans in them, code that matches what the package claims to do.

npm view some-package repository.url homepage

A link that 404s, or points at a repository whose contents do not match the published tarball, is the cheapest tell there is. The published tarball and the linked repository are not verified against each other by npm.

What it proves: somebody maintained a codebase. What it misses: whether that code is what got published. This is the gap npm provenance exists to close, and it is narrower than it sounds — see check 6.

5. Look at the dependency tree, not just the package#

npm ls --all
npm view some-package dependencies

You are not installing one package. You are installing its transitive closure, and every one of those is a publisher who can be compromised. A utility that pulls forty dependencies has forty times the surface of one that pulls none.

Watch specifically for dependencies specified as git URLs or tarball URLs rather than registry ranges. Those bypass the registry entirely — no integrity hash, no version history, and prepare runs on clone.

What it proves: the size of your exposure. What it misses: nothing about whether any of it is malicious. This is a measurement, not a check.

6. Check provenance and attestation — and know what they cover#

npm view some-package dist.attestations

npm provenance links a published package to the CI job that built it. That is genuinely useful: the strongest single signal of account takeover is a version published without the attested pipeline every previous release used. A drop in attestation between versions is worth stopping for.

But read what it attests. Provenance proves the build, not the source. It tells you these bytes came out of that workflow in that repository. It does not tell you the source going in was clean, and a maintainer who compromises their own repository produces a perfectly attested malicious package.

What it proves: which pipeline produced the artifact. What it misses: whether the input to that pipeline was honest.

7. Scan for known vulnerabilities — last, not first#

npm audit

This belongs at the end because of what it is: a lookup against already-disclosed advisories, run against packages you have already installed. It is excellent at finding the CVE from eight months ago in a transitive dependency. It is structurally incapable of catching the package uploaded this morning, because nobody has filed anything about it yet.

What it proves: you are not carrying known-bad versions. What it misses: everything novel. Malware's useful life is measured in hours, and disclosure is measured in days.

What passes all seven#

Run every check above and these still get through:

AttackWhy the checklist misses it
Maintainer sabotageNo name mismatch, no install script, no provenance drop. The package is exactly what it claims, by the person entitled to publish it. colors (2022) and node-ipc's peacenotwar are the reference cases.
A clean package that turns hostile laterEvery check above describes the version in front of you. A release being clean says nothing about the one published after it.
Payload behind require()--ignore-scripts stops install-time execution. It does not stop the code from running the first time you import it.
Prompt injection aimed at your agentNothing in the list reads the README as a target. If an AI agent is doing the installing, the README is an input to a system that acts on text.

That last row is the one that changed recently, and it is worth being concrete about. When a coding agent installs a dependency, the package's README, description and manifest land in a context window belonging to something with shell access. Charlie Eriksen traced a hallucinated package, react-codeshift, propagating through 237 repositories via agent-generated skills — the agent hallucinated the name, wrote the install command, and ran it without a human ever reading it.

The honest summary#

The seven checks are worth running. Together they catch the large majority of what is actually published against you, and checks 1 and 3 alone catch most of it.

What they cannot do is run on every transitive dependency of every install, every time, which is the only cadence that matters. A checklist executed by a human on the package they are thinking about is not a control — it is a habit, and the packages that hurt you are the ones you were not thinking about.

That is the argument for automating the gate rather than the checklist, whether with npq, a registry proxy, or DepWall. What matters is that something runs on the installs you did not stop to consider.

And whatever you choose: hold it to stating what it missed. A tool that only publishes its wins is telling you about its test suite, not about your risk. Ours allowed 69% of live OSV-flagged malware on metadata alone, which is why the judge exists and why that number is on this site.