npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vetdeps

v1.0.5

Published

Tells you whether a package you are about to install is the one you think it is. Catches typosquats, AI-hallucinated package names and known malware. Zero dependencies, zero telemetry.

Readme

vetdeps

Tells you whether a package you are about to install is the one you think it is.

npm version provenance CI license

zero dependencies zero telemetry node types

npx vetdeps

Checking whether a package "exists" stopped working

AI coding tools suggest package names that do not exist about 19.7% of the time, and 43% of those hallucinated names repeat across identical prompts. Attackers know this. They register the names and wait.

So here are three documented AI-hallucinated package names, checked against the npm registry right now:

react-codeshift   HTTP 200
react-fetch-hook  HTTP 200
unused-imports    HTTP 200

All three exist. Every guard built on an existence check reports them safe.

What actually separates them is reputation:

| Package | First published | Downloads/week | Reality | | --- | --- | --- | --- | | react-codeshift | 2026-01-14 | 1 | hallucinated name, registered by someone else | | unused-imports | 2025-10-27 | 201 | squats eslint-plugin-unused-imports | | eslint-plugin-unused-imports | 2019-09-18 | 7,780,771 | the real package | | react-fetch-hook | 2018-12-18 | 15,329 | genuinely fine, despite sounding invented |

That last row is the hard part. A rule like "new or odd-sounding name equals bad" flags a perfectly good package, and a security tool that cries wolf gets uninstalled the same day.

Postscript, and it makes the point better than any pitch: while this tool was being built, unused-imports graduated from suspicious name to confirmed malware. It now carries OSV advisory MAL-2025-48781. So does types-node (MAL-2024-12159). The names picked as typosquat examples turned out to be real attacks.


Quick start

# audit everything in this project
npx vetdeps

# check one package before you add it
npx vetdeps unused-imports

# check, then hand over to npm if it is clean
npx vetdeps install express

Install it properly to get the automatic surfaces:

npm install --save-dev vetdeps

npx vetdeps init          # gate every npm install in this project
npx vetdeps init --agent  # stop your AI agent installing a bad package

Why this one fires by itself

Twelve packages already tried to solve this. The most successful reached 202 weekly downloads. They are all scanners you have to remember to run, and in 2026 the thing typing npm install is often an agent that never pauses to sanity check a name.

So vetdeps installs into the places where installs actually happen:

| Surface | What it does | Setup | | --- | --- | --- | | Preinstall gate | Runs on every npm install in the project | npx vetdeps init | | AI agent hook | Blocks Claude Code / Cursor before it installs | npx vetdeps init --agent | | CI | Fails the build on a critical finding | npx vetdeps --ci | | CLI | Manual check | npx vetdeps <package> |


Use cases

1. Stop an AI agent installing a hallucinated package

The single most valuable one. Your agent writes npm i unused-imports because a model suggested it. Without a gate, it just runs.

npx vetdeps init --agent

That writes a PreToolUse hook into .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [{ "type": "command", "command": "npx --no-install vetdeps hook" }] }
    ]
  }
}

It goes through npx on purpose. node_modules/.bin is not on PATH when a hook runs, so a bare vetdeps hook exits 127 and guards nothing. --no-install means a missing install fails loudly rather than quietly downloading itself mid-hook.

Hooks are picked up live, so there is no need to restart your session. Now the agent gets stopped:

Blocked by vetdeps. Do not install this.

  critical  unused-imports
            known malicious package: MAL-2025-48781

  1 critical, 0 warnings, 0 ok

Check the package name against the real one before trying again.

2. Gate every install in a project

npx vetdeps init

Adds this to package.json:

{
  "scripts": {
    "preinstall": "node -e \"import('vetdeps/guard').catch(() => {})\""
  }
}

The catch is deliberate. npm runs preinstall before dependencies are installed, so on a fresh clone the vetdeps binary does not exist yet. Without that catch, this gate would break npm install for every new contributor. It fails open on a fresh clone and runs on every install after.

Bypass once, if you genuinely need to:

VETDEPS_OFF=1 npm install

3. Fail CI on a critical finding

# .github/workflows/ci.yml
- run: npx vetdeps --ci

--ci is an alias for --strict, which promotes warnings to failures too.

Exit codes:

| Code | Meaning | | --- | --- | | 0 | clean, or warnings only (default) | | 1 | a critical finding, or any finding under --strict | | 2 | usage error |

4. Check before adding a dependency

$ npx vetdeps "@ctrl/[email protected]"

  critical  @ctrl/[email protected]
            known malicious package: MAL-2025-47141

  1 critical, 0 warnings, 0 ok
$ npx vetdeps lodashs

  warning   lodashs
            looks like lodash, but has only 0 weekly downloads

  0 critical, 1 warning, 0 ok

5. Machine readable output

npx vetdeps express --json
[
  {
    "name": "express",
    "version": "4.18.2",
    "level": "ok",
    "reasons": []
  }
]

Use it as a library

Everything is exported and typed.

import { auditPackages, loadCorpus } from "vetdeps";

const verdicts = await auditPackages(
  [{ name: "unused-imports", version: "1.0.0" }],
  { corpus: loadCorpus() }
);

for (const v of verdicts) {
  if (v.level === "critical") {
    throw new Error(`${v.name}: ${v.reasons.map((r) => r.message).join(", ")}`);
  }
}

Score facts you already have, with no network at all:

import { scorePackage, loadCorpus } from "vetdeps";

const verdict = scorePackage({
  name: "unused-imports",
  weeklyDownloads: 201,
  exists: true,
  createdAt: "2025-10-27T00:00:00.000Z",
  versionCount: 1,
  hasInstallScript: false,
  repository: null,
  advisories: []
}, loadCorpus());

console.log(verdict.level);   // "warn"
console.log(verdict.reasons); // [{ code: "name-confusion", message: "looks like ..." }]

Just the name-confusion rules:

import { findConfusableMatches, loadCorpus } from "vetdeps";

findConfusableMatches("unused-imports", loadCorpus());
// [{ rule: "prefix-drop", target: "eslint-plugin-unused-imports", distance: 0 }]

findConfusableMatches("chalk-cli", loadCorpus());
// [] - legitimate companion package, deliberately not flagged

Audit a project directory:

import { readProject, auditPackages, loadCorpus, renderReport } from "vetdeps";

const verdicts = await auditPackages(readProject(process.cwd()), { corpus: loadCorpus() });
process.stdout.write(renderReport(verdicts));

What it checks

| Signal | Source | | --- | --- | | Known malware, version precise | OSV.dev MAL- advisories | | Package does not exist | npm registry | | Name confusable with a popular package | bundled corpus of 17,338 high-impact names | | Real adoption | npm downloads API | | Brand new package | npm registry | | Runs install scripts | npm registry |

The name rules, and why plain edit distance fails

unused-imports and eslint-plugin-unused-imports are fifteen edits apart. Every typosquat tool built on Levenshtein distance misses the most common real case.

| Rule | Catches | Example | | --- | --- | --- | | Dropped ecosystem prefix | The most common LLM hallucination shape | unused-imports to eslint-plugin-unused-imports | | Dropped scope | Scope stripped from a scoped package | types-node to @types/node | | Typo distance | Classic typosquats | lodahs to lodash | | Reordered words, separators | Shuffled or re-punctuated names | fetch-node to node-fetch |

Two design decisions keep the false positive rate at zero:

Edit distance scales with name length. A fixed threshold of 2 flagged znv as a typo of ajv. Short names sit naturally close together, so names under 5 characters need an exact match.

Affix rules only add an affix, never strip one. Stripping flagged chalk-cli as chalk and vite-plugin-vue as vue, both legitimate, because <tool>-plugin-<x> and <tool>-cli are normal naming conventions.

And every name rule is gated on adoption. A match only becomes a warning if the package has no real usage of its own to vouch for it.

Measured on a real 550 package project: 0 false positives.


What it will not do

  • No dependencies. A supply chain security tool that ships its own dependency tree is a punchline. dependencies is empty and stays empty.
  • No telemetry. Nothing about you or your project is sent anywhere. There is no analytics, no phone home, no install ping.
  • Your private packages stay private. Anything resolving to a registry other than registry.npmjs.org is skipped and never sent to a public API. Your internal module names are not somebody else's data.
  • It will not break your install. Registry down, rate limited, offline, malformed response: vetdeps warns and gets out of the way. Unknown is never treated as risk. Only --strict changes that.

Performance

Measured on a real 550 package lockfile (express, typescript, eslint, webpack, jest, react, react-dom, vite):

| | Time | | --- | --- | | Cold, empty cache | 2.1s | | Warm | 0.9s |

The first working version took 56.7 seconds. Nobody keeps a 57 second preinstall hook, and a deleted hook protects nobody, so this got fixed before anything else. Registry lookups are cached at ~/.cache/vetdeps. Advisory lookups deliberately are not cached, because popular packages are exactly the ones that get compromised, which is what happened to chalk, debug, @ctrl/tinycolor and axios.


Known limits

Stated plainly, because a security tool that oversells itself is worse than none.

  • MAL- is the malware signal. OSV's MAL- feed is the only machine-readable "this is malware" marker available. GHSA advisories are surfaced as information but only count as malware when the summary says so outright. A compromise that received only a GHSA whose wording avoids the word malicious is reported as an advisory, not as malware. event-stream is the worked example.
  • No source code analysis. vetdeps reads metadata and advisories. It does not download or inspect tarballs, so it cannot detect novel malicious code nobody has reported yet.
  • CVE scanning is not the job. That is npm audit. This tool answers a different question: is this the package you meant?
  • Lockfiles supported: package-lock.json, pnpm-lock.yaml, yarn.lock (classic v1) and bun.lock, falling back to package.json. Yarn berry (v2+) lockfiles are not read, because they usually drop the registry URL a private package needs to be told apart from a public one. The binary bun.lockb written by older bun versions is not read either; run bun install --save-text-lockfile to get a text bun.lock vetdeps can use. Contributions welcome.

Found a false positive?

That is the most valuable bug you can file, and it is treated as a defect, not a tuning request. A tool that flags a legitimate package gets deleted.

Report a false positive

Found a package it missed?

Report a miss

Anything else

Found a security problem in vetdeps itself? Please read SECURITY.md and do not open a public issue.


Contributing

Contributions are welcome. See CONTRIBUTING.md for the full guide.

git clone [email protected]:Tisankan-dev/vetdeps.git
cd vetdeps
npm install
npm run check     # type check, build, and run the full test suite

Two rules that are not negotiable, because they are the product:

  1. No runtime dependencies. Ever.
  2. A new detection rule must come with measured evidence. Show what it catches and, more importantly, show it flags nothing legitimate.

Author

Tisankan Jeyakumar Chief Technical Officer, Yarl Ventures (PVT) Ltd, Sri Lanka

Website GitHub Email


License

MIT © Tisankan Jeyakumar