To verify an npm package before installing it, inspect the published tarball (not the GitHub repo), check for lifecycle scripts, verify provenance attestations, and confirm the maintainer and repository actually match — the whole routine takes about four minutes and catches nearly every known compromise pattern. npm install runs arbitrary code as your user, at install time, before you have imported anything. That is the threat model. Here is the routine.
Inspect what will actually be installed
The GitHub repository is marketing. The tarball on the registry is what runs. They are not guaranteed to match, and several real attacks — the event-stream incident being the classic — shipped malicious code only in the published artifact. Look at the artifact:
# Metadata: version list, publish dates, maintainers, tarball URL
npm view left-pad
# Download and unpack the tarball without installing
npm pack left-pad@1.3.0
tar -tzf left-pad-1.3.0.tgz # list contents
tar -xzf left-pad-1.3.0.tgz && less package/package.json
Red flags at this stage: minified or obfuscated files in a package that claims to be plain source, files that do not exist in the repo (build.js doing network calls), and a main entry pointing somewhere unexpected. Also check the publish timeline in npm view <pkg> time --json — a package dormant for three years that shipped a patch release yesterday deserves a closer look before you take that release.
Check for lifecycle scripts first
Install scripts are the mechanism almost every malicious npm package uses, because preinstall and postinstall execute automatically:
npm view some-package scripts
If you see preinstall, install, or postinstall, read them. Legitimate uses exist — native modules compiling with node-gyp, packages downloading prebuilt binaries — but "legitimate" still means "code executing on your machine from the network," so read the script and whatever file it invokes.
Better: make scripts opt-in globally and allowlist the exceptions.
npm config set ignore-scripts true
With that set, packages that genuinely need scripts (esbuild, sharp, and friends) get handled explicitly. pnpm made this the default posture in v10, which tells you where the ecosystem consensus landed.
Verify provenance and registry signatures
Since npm shipped provenance attestations, packages built via supported CI (GitHub Actions, GitLab CI) can carry a Sigstore-backed statement linking the artifact to a source commit and a build workflow. Check it:
# Verify signatures and provenance for everything in the lockfile
npm audit signatures
# Inspect a single package's attestation
npm view react dist.attestations --json
A green npm audit signatures tells you the tarballs match the registry's signed record and any provenance attestations verify against the public transparency log. It does not tell you the code is benign — a malicious maintainer can produce perfectly attested malware — but it kills the "artifact swapped after publish" and "built from sources that do not match the repo" attack classes. For anything that will run in CI with credentials, treat missing provenance on a popular, actively-maintained package as a question worth asking.
Read the human signals
Two minutes on the registry page and the repo answers most of the rest:
| Signal | Healthy | Worth pausing on |
|---|---|---|
| Maintainers | Known org or long-lived accounts | Ownership transferred last month |
| Repo link | Resolves, tags match versions | 404s, or tags stop two versions ago |
| Release cadence | Regular, with changelogs | Silent burst after long dormancy |
| Weekly downloads | Consistent with claimed adoption | 40 downloads but "used by thousands" |
| Name | The one you meant | One transposed character from it |
Typosquatting still works in 2025 because people install from memory. npm view the exact name before you install it, especially for scoped-sounding names — attackers register the unscoped twin of popular scoped packages and wait. Reconstructing how these compromises unfold is its own discipline; our post on dependency compromise timeline reconstruction walks through a real one.
Check the dependency tree you are adopting
You are not installing one package; you are installing its closure:
npm view some-package dependencies
npm install --dry-run some-package # full tree, nothing written
A utility library pulling 60 transitive dependencies is a maintenance liability even when every one of them is clean today. Each is a future compromise vector and a future CVE ticket. When two candidates solve the same problem, the shallow tree wins.
Make it policy instead of heroics
Manual vetting does not scale past a team of three, and it never survives a deadline. The durable version of this routine is enforcement in the path of installation: ignore-scripts in a committed .npmrc, lockfile linting (lockfile-lint --path package-lock.json --allowed-hosts npm) so nobody swaps a tarball URL, and a scanner in CI that knows about malicious-package feeds rather than only CVEs. That last part is where SCA tooling earns its keep — Safeguard flags typosquats, install-script anomalies, and known-bad versions on the pull request that introduces them, which is roughly the four-minute routine above, automated on every diff. If you want to go deeper on the attacker's side of this, the Academy has a module on npm attack patterns.
Frequently asked questions
Is npm audit enough to vet a new package?
No. npm audit matches versions against published advisories, so it only catches problems someone has already found and reported. A brand-new typosquat or a freshly compromised release passes npm audit cleanly for days.
What is the single fastest check with the best payoff?
npm view <package> scripts. Install-time script execution is the dominant delivery mechanism for malicious npm packages, and the check takes ten seconds.
Do provenance attestations mean a package is safe?
They mean the artifact verifiably came from a specific repo and build workflow. That eliminates artifact tampering, but says nothing about the intent of the code. Provenance answers "where did this come from," not "is this good."
Should I pin exact versions instead of using ranges?
Your lockfile already pins the full tree; commit it and use npm ci in CI. Exact versions in package.json add a little clarity but the lockfile is what actually determines installs.