Every npm install is a trust decision. Here's how to make it an informed one — from package metadata to provenance attestation, install scripts, and audit tools.
TL;DR: Don't trust npm packages blindly. Check publish date, install scripts (npm view), weekly downloads, bundle size (npm pack --dry-run), GitHub health indicators, and run npm audit. Use npm install --allow-scripts=pkg1,pkg2 with npm 11.16+ to keep install scripts opt-in. The Mini Shai-Hulud attack (May 2026) is the latest reminder of what can go wrong — see my analysis of the Shai-Hulud npm supply chain attack for the full story.
Every time you run npm install , you're granting that package's code — and every transitive dependency it brings along — full access to run on your machine, your build server, and potentially your production environment. The npm ecosystem is built on trust, and trust is exactly what attackers exploit.
The Mini Shai-Hulud supply chain attack in May 2026 compromised over 170 packages including TanStack, Mistral AI, and UiPath. Attackers gained access through maintainer account takeovers and used postinstall scripts to exfiltrate credentials and inject malicious code into production builds. This wasn't a theoretical attack — it affected real companies with real security teams.
More recently, the Red Hat Shai-Hulud backdooring incident in npm packages served as yet another wake-up call. Both incidents shared a common pattern: attackers targeted packages with high download counts but low maintainer oversight, used install scripts as the delivery mechanism, and relied on the fact that most developers never look at what actually runs during npm install.
The good news? npm 11.16.0 introduced --allow-scripts flags making install scripts opt-in by default. The ecosystem is moving toward better security defaults, but no CLI switch replaces developer judgment. This guide walks through every signal you should evaluate before adding a dependency to your project.
A package with 50,000 GitHub stars can be abandoned for three years. Stars measure popularity, not quality or safety. Here are the metrics that actually correlate with a well-maintained package.
The single most important signal. Check when the package was last published to npm:
npm view <package> time --json | tail -5
For actively maintained packages, expect a publish within the last 6 months. Packages untouched for 2+ years are red flags — they contain known CVEs, don't support modern Node.js versions, and won't get security patches when new vulnerabilities emerge.
Weekly downloads indicate community adoption. A package with 100,000+ weekly downloads has been battle-tested across thousands of projects. That said, download counts can be inflated by CI/CD systems that reinstall on every pipeline run. Cross-reference with dependents count:
npm view <package> dependents
npm view <package> description version downloads
Higher dependents means other package authors trust the API enough to build on top of it — a stronger signal than raw download count alone.
A surprisingly large package may do more than you need — or include unnecessary dependencies. Check unpacked size and file count:
npm view <package> --json | grep -E "dist|unpackedSize"
For a visual comparison, use npm pack <package> --dry-run to see every file that will be installed. If a simple utility package ships 50+ files, ask why.
Pro tip: Use bundlephobia.com to check minified + gzip sizes before installing. A package that looks small in node_modules may have a heavy impact on your JavaScript bundle if it's loaded on the client side.
npm lifecycle scripts (preinstall, postinstall, preuninstall) are shell scripts that run with your user permissions during npm install. They can read environment variables, access the filesystem, make network requests, and install additional software. They are the #1 attack vector in supply chain attacks.
# Check if a package has lifecycle scripts
npm view <package> scripts
# See all files before installing
npm pack <package> --dry-run
If the scripts field contains postinstall without a clear node build.js or JS-based purpose, investigate further. Native modules (node-gyp rebuild) are legitimate reasons for postinstall scripts — but any script that downloads external resources or accesses the network is suspect and should be verified.
--allow-scriptsnpm 11.16.0 introduced a major security improvement: the --allow-scripts flag makes install scripts opt-in by default. Instead of all postinstall scripts running automatically, only explicitly approved packages can run them:
# Install with opt-in scripts (npm 11.16+)
npm install <package> --allow-scripts=<package>
This single change blocks the most common supply chain attack vector — a compromised package that quietly adds a malicious postinstall script. For a deeper dive into how this RFC evolved and what alternatives exist for package authors who previously relied on postinstall, see my analysis of the npm install scripts opt-in RFC.
Provenance attestation is npm's answer to the question "was this package actually published by its maintainer from the real source code?" It uses Sigstore transparency logs and SLSA Build Level 2+ to cryptographically link a published package to a specific CI build job.
When a package has provenance, you can verify it was built on GitHub Actions (or another supported CI) from a specific repository commit:
# Check provenance for a package
npm view <package> --json | jq '.dist.attestations'
# Verify provenance at install time
npm install <package> --verify-provenance
Not every package publishes provenance yet — it requires explicit CI configuration and GitHub's OIDC token. But major frameworks and tooling are adopting it. When available, preferred provenance over non-provenance packages, especially for critical infrastructure like build tools, bundlers, and security libraries.
A package's GitHub repository tells you more than its npm page ever will. Here's what to inspect before trusting a package with your production environment.
A healthy project has commits within the last 3-6 months. Check the pulse of the repository: are commits regular, or do they come in bursts around release time? Burst commits followed by long silences suggest dependency-driven updates rather than active maintenance.
Go to the GitHub Issues tab and sort by "Most commented" or use GitHub's recently-updated filter. A project with 200 open issues and no responses from maintainers is a dead project. Healthy projects have a managing issue response time under 7 days and actively prune stale issues.
A README without CI badges is a red flag. Look for GitHub Actions, CircleCI, or similar build status badges. Also check for code coverage, linting, and security scanning in the CI pipeline. A project that doesn't run tests before merging pull requests will inevitably ship bugs.
Bus factor matters. A project with a single maintainer who hasn't touched the repo in 8 months is a single point of failure. Projects with multiple maintainers (3+) from different organizations have better bus factor and typically more responsive issue triage.
No single tool catches everything. The best dependency safety strategy combines several scanning approaches.
| Tool | What It Checks | Strengths | When to Use |
|---|---|---|---|
| npm audit | Known CVEs against npm advisory database | Built-in, zero setup, Snyk-backed advisory data | Every install, every CI run |
| Socket.dev | Security + quality signals (install scripts, typosquatting, network access, suspicious patterns) | Detects supply chain risks that don't have CVEs yet, PR bot for new dependencies | PR review gate, new package evaluation |
| Snyk | Known vulnerabilities with fix guidance | Broad database, integration with container scanning, license compliance | CI pipeline, monthly deep audit |
| OpenSSF Scorecard | Open-source project security practices (CI tests, SAST, token permissions, maintenance) | Google-backed, checks security posture of the package's development process itself | Pre-dependency decision for new packages |
| npm query | Dependency tree structure and composition | SQL-like queries on your node_modules, finds packages with install scripts, deprecated packages, duplicates | Dependency tree audit, post-install inspection |
# CI pipeline check (fail on critical)
npm audit --audit-level=critical
# Socket.dev CLI scan
npx socket scan
# Find all packages with install scripts
npm query ".scripts:not(.scripts == {})"
# Find all deprecated packages
npm query ".deprecated"
The npm query command is especially useful — it uses CSS-like selectors to search your dependency tree. You can find packages with specific properties, versions, or from specific authors:
# Packages with postinstall scripts
npm query ":attr(.scripts, [postinstall])"
Packages don't stay healthy forever. Watch for these deprecation signals in your existing dependencies:
When you identify a deprecating package, plan a migration. Don't wait for a critical CVE to force your hand. The best time to replace an unmaintained package is while you have time to evaluate alternatives properly.
A single direct dependency often pulls in dozens of transitive dependencies. That utility library you added for one function? It might bring 50 packages along for the ride.
# Show the full tree
npm ls --all
# Show only production dependencies
npm ls --all --prod
# Show why a specific package is included
npm ls <package-name>
For large projects, npm ls --all can be overwhelming. Use npm ls <package> to trace exactly why a particular dependency ended up in your project — which direct dependency pulled it in.
Always check whether you actually need a development dependency in production. Tools like webpack, typescript, and eslint should be in devDependencies only. Putting them in dependencies bloats your production node_modules and increases attack surface.
# Count packages in each category
npm ls --prod | wc -l
npm ls --dev | wc -l
If your production dependency count is more than 10x your direct dependencies, your dependency tree has unnecessary depth — consider tools like npm dedupe or a monorepo with shared packages.
Before adding any new npm package to your project, run through this checklist. It takes 2-3 minutes per package and has saved me from adding abandoned, bloated, or suspicious packages more times than I can count.
npm installnpm view <package> time — within 6 months?npm view <package> scripts — has postinstall?npm view <package> downloads — 1,000+?npm view <package> license — MIT, Apache-2.0, or compatible?npm pack <package> --dry-run — reasonable file count?npm view <package> --json | grep attestationsFor packages the entire team will rely on (frameworks, bundlers, state management, security libraries), spend an additional 10 minutes: read the README thoroughly, check issue response times, review the last 10 commits, and check if the package has been audited by a third party.
License compatibility matters more than most developers realize. Your project's license must be compatible with every dependency's license. The most common conflict is GPL-licensed packages in proprietary software — GPL's copyleft provisions can force you to open-source your entire application.
# Check a package's license
npm view <package> license
# Get license list for all packages
npm licenses
# Or with npm-license-audit-cli for JSON output
npx license-checker --json
Safe defaults for commercial projects: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Unlicense. Use these licenses as your acceptance criteria unless you have a legal team reviewing every dependency.
Let's look at a practical example. Say you're evaluating zod for schema validation. Here's what the signals tell us:
This is what a trustworthy package looks like across every dimension. When a package fails two or more of these checks, it's worth asking if you really need it, or if a simpler alternative exists.
npm audit on your project after install.--allow-scripts to make install scripts opt-in. For a detailed analysis of the attack, see my Shai-Hulud attack breakdown.npm view <package> scripts to see lifecycle scripts. You can also inspect contents with npm pack <package> --dry-run which lists all files without downloading. For script content, extract and read the package's package.json or use npm query ".scripts:not(.scripts == {})" after installation.npm audit (built-in, CVE scanning), Socket.dev (supply chain risk detection, install scripts, typosquatting), Snyk (broad vulnerability database with fix guidance), OpenSSF Scorecard (project security practices evaluation), and npm query (dependency tree analysis). Use them together — no single tool catches every risk. Run basic checks (npm audit) on every CI run and deeper scans (Socket.dev, Scorecard) when evaluating new critical dependencies.npm audit on every install and as part of your CI pipeline (fail on critical vulnerabilities). Do a full dependency review at least monthly — check for deprecated packages, new CVEs, abandoned transitive dependencies, and dependency tree bloat. Major version bumps and framework upgrades are natural triggers for a comprehensive dependency review. The npm install scripts opt-in RFC is a good example of how the ecosystem is evolving to reduce audit burden — see my analysis of the RFC.Evaluating npm packages is just one piece of building secure, maintainable web applications. Every project I build follows a structured approach to dependency management, security audits, and long-term maintenance planning. If you're planning a new project or want to improve an existing codebase, reach out to me for a free consultation.
I'm a full-stack developer with over 20 years of experience building production applications in React, Vue, Node.js, and beyond. Based in Minsk and working worldwide — let's discuss your project.
Tell me about your project — I'll evaluate your tech stack and provide a preliminary estimate. Free of charge.