DEV Community

Tisankan
Tisankan

Posted on Originally published at github.com

Checking whether an npm package "exists" stopped working

Every npm supply chain postmortem ends with the same advice: verify the package before you install it.

So I looked at what the tools that do this actually check. Most of them check whether the package exists.

That check is dead. Here are three package names documented as AI hallucinations, against the npm registry right now:

react-codeshift   HTTP 200
react-fetch-hook  HTTP 200
unused-imports    HTTP 200
Enter fullscreen mode Exit fullscreen mode

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

Why this happens

AI coding tools suggest package names that do not exist about 19.7% of the time, measured across a 576,000 sample study. More usefully for an attacker, 43% of those hallucinated names repeat across identical prompts.

That is the whole attack. The hallucinations are predictable, so you register the common ones and wait for someone to paste an install command. It has a name now: slopsquatting.

And in 2026 the thing typing npm install is often an agent that never pauses to sanity check a name at all.

What actually separates good from bad

Not existence. Reputation.

Package First published Downloads/week
react-codeshift 2026-01-14 1
unused-imports 2025-10-27 201
eslint-plugin-unused-imports 2019-09-18 7,780,771
react-fetch-hook 2018-12-18 15,329

Look at that last row, because it is the hard part.

react-fetch-hook sounds completely invented. It is seven years old, has 15,000 weekly downloads, and is entirely legitimate. Any rule of the form "new or odd-sounding name equals bad" flags it immediately.

And a security tool that flags a legitimate package gets uninstalled the same day. False positives are not a tuning problem, they are the product failing.

Plain edit distance does not work either

This surprised me most.

unused-imports and eslint-plugin-unused-imports are fifteen edits apart.

Every typosquat checker built on Levenshtein distance sails straight past the single most common real case. The rule that actually catches it is different:

Does the candidate, plus a known ecosystem prefix, equal a popular package?

unused-imports + eslint-plugin- = a package with 7.8 million weekly downloads. That is exactly the shape an LLM produces when it drops the ecosystem prefix, which it does constantly.

Two rules I got wrong, and how I found out

I measured every rule against real packages before shipping. Two of them were wrong, and I would never have reasoned my way to either.

A fixed edit distance of 2 flagged znv as a typo of ajv. Both real, both legitimate. Short names sit naturally close to each other, so a fixed threshold is meaningless on them. Allowed distance now scales with name length: under 5 characters requires an exact match.

Affix rules that stripped a suffix flagged chalk-cli as chalk. chalk-cli is Sindre Sorhus's official CLI, published 2015. It also flagged vite-plugin-vue as vue. Of course it did: <tool>-plugin-<x> and <tool>-cli are just how the ecosystem names things. Affix rules now only add an affix, never strip one.

The lesson generalises: if you write detection rules and do not measure them against real, legitimate packages, you are not building a security tool. You are building a noise generator.

Final measurement on a real 550 package project: 0 false positives.

A postscript that makes the point better than I can

While I was building this, unused-imports graduated from suspicious name to confirmed malware. It now carries OSV advisory MAL-2025-48781.

So did types-node, with MAL-2024-12159.

The two names I had picked as typosquat examples turned out to be real, confirmed attacks.

The tool

npx vetdeps
Enter fullscreen mode Exit fullscreen mode
npx vetdeps unused-imports     # check one package before you add it
npx vetdeps init               # gate every npm install in this project
npx vetdeps init --agent       # stop your AI agent installing a bad package
npx vetdeps --ci               # fail the build on a critical finding
Enter fullscreen mode Exit fullscreen mode

What a hit looks like:

  critical  @ctrl/tinycolor@4.1.1
            known malicious package: MAL-2025-47141

  1 critical, 0 warnings, 0 ok
Enter fullscreen mode Exit fullscreen mode

The part I think actually matters

Twelve packages already occupy this niche. The most successful has 202 weekly downloads. Not one broke through.

They are all scanners you have to remember to run. That is a losing bet, because the habit that already exists always beats the habit you are asking someone to form. And the installer in 2026 is frequently not a human at all.

So vetdeps installs into the places where installs actually happen: a preinstall gate that fires on every npm install, and a Claude Code / Cursor hook that blocks an agent before the install runs.

npx vetdeps init --agent
Enter fullscreen mode Exit fullscreen mode
Blocked by vetdeps. Do not install this.

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

Check the package name against the real one before trying again.
Enter fullscreen mode Exit fullscreen mode

What it will not do

  • No dependencies. A supply chain security tool that ships its own dependency tree is a punchline.
  • No telemetry. Nothing about you or your project goes anywhere.
  • Your private packages stay private. Anything not resolving to registry.npmjs.org is skipped and never sent to a public API.
  • It will not break your install. Registry down, rate limited, offline: it warns and gets out of the way.

Honest limits

Overselling a security tool is worse than not shipping one.

  • No source code analysis. It reads metadata and advisories, so novel unreported malware is not detected.
  • MAL- is the only machine readable malware signal OSV offers. A compromise that received only a GHSA whose wording avoids the word "malicious" is surfaced as an advisory, not as malware. event-stream is the worked example.
  • CVE scanning is not the job. That is npm audit. This answers a different question: is this the package you meant?
  • Yarn berry is deliberately unsupported. Berry lockfiles omit resolved URLs, so there is no way to tell a public package from a private one. Supporting it would mean defaulting everything to "public" and leaking private package names to a public API. npm, pnpm, yarn v1 and bun all work.

Try it

npx vetdeps
Enter fullscreen mode Exit fullscreen mode

github.com/Tisankan-dev/vetdeps

If it flags something legitimate in your project, I genuinely want to know. There is an issue template for exactly that, and I treat those as defects rather than tuning requests.

Top comments (0)