The decision that matters happens before the install. The registry serves a JSON document per package, so a short script turns a name into facts.
const res = await fetch(`https://registry.npmjs.org/${process.argv[2]}`);
const doc = await res.json();
const latest = doc['dist-tags'].latest;
const v = doc.versions[latest];
console.log(JSON.stringify({
version: latest,
date: doc.time[latest].slice(0, 10),
license: v.license,
deps: Object.keys(v.dependencies ?? {}).length,
maint: (doc.maintainers ?? []).length,
attested: Boolean(v.dist.attestations)
}));> node check.mjs pino
{"version":"10.3.1","date":"2026-02-09","license":"MIT","deps":11,"maint":4,"attested":true}attested: true is the strongest single signal: the tarball carries a Sigstore 69,885 provenance attestation tying it to a public commit and workflow (Trusted Publishing). A package with a repository, several maintainers and provenance is a very different risk from one published three days ago by a lone account. Then weigh:
Bus factor. One maintainer is one account to phish; four means an attacker needs luck.
Maintenance. Last publish date, open issues, whether the repository is archived.
Dependency count. npm 2,036 ls --all --omit=dev turns a "small helper" into a 40-package subtree.
License. PM2 29,762 , for instance, is AGPL-3.0, with real obligations.
Whether you need it. node:crypto, structuredClone and util.parseArgs replaced many micro-packages.
npq 1,796 (https://github.com/lirantal/npq 1,796 ) (Apache-2.0, npm i -g npq) automates most of that list: npq install checks package age, downloads, install scripts, new or dormant maintainers, typosquatting, signatures, provenance and deprecation, then asks for confirmation before calling npm. Socket CLI 320 (https://github.com/SocketDev/socket-cli 320 ) (MIT, @socketsecurity/cli) instead analyzes what the code does — filesystem, network, shelling out — flagging packages with no advisory yet.