Dekita

Your agent just added a package that does not exist — run this before npm install

security ai node devtools

Here is the moment this post is about. The agent's diff looks fine, so you accept it and run npm install. The dev server starts, the tests pass, you move on. What nobody checked is the one thing the agent cannot check for you: whether the package it just added actually exists.

Not whether it is the best package. Whether it exists at all.

Why this is not paranoia

In 2025, Spracklen and colleagues published "We Have a Package for You!" at USENIX Security. They took 16 code-generating models, had them write code against two prompt datasets in Python and JavaScript, and analyzed 576,000 generated samples for package references that do not exist in the official registries.

The headline: 19.7% of recommended packages were hallucinations. Across the sample they collected 205,474 unique package names that no registry had ever hosted. The rate varied by model class — 5.2% for commercial models, 21.7% for open-source ones — but no model in the study was clean.

A fake package name is only interesting if someone can profit from it. That is where the second finding matters. When the researchers re-ran identical prompts ten times, 43% of the hallucinated names reappeared on every single run, and 58% reappeared on more than one (summarized in the Cloud Security Alliance's 2026 research note on slopsquatting). Hallucination is not random noise; a meaningful share of it is deterministic. An attacker who knows which model you use can register the names it will confidently invent, and wait.

Waiting is not theoretical. The CSA note documents the unused-imports package — a stand-in for the real eslint-plugin-unused-imports — which npm security-held yet still recorded roughly 233 weekly downloads as of early February 2026. It also documents huggingface-cli, which passed 30,000 downloads in three months after an AI-recommended install command was copied into public documentation without verification.

So the question for your Tuesday afternoon is not "could an attacker target me". It is: when the agent adds a dependency, what in your workflow would notice?

Why npm audit does not cover this

The standard answer is npm audit. It is worth running. It also cannot see this problem: advisory databases know about reported issues, and a package registered last week with no history has no advisory. The dangerous window is the gap between registration and the first report — days to weeks, which is plenty of time for a postinstall script to run.

Isaac Bell made this point in a dev.to post on October 9, 2026, "Before you run the code your AI agent wrote, check these five things". His first check is dependencies you did not ask for: "Assistants sometimes suggest package names that don't exist, or that are one letter off a real one. Attackers register those names." His post is worth reading in full — this article automates exactly his first check and nothing else.

The check

A script. Node 18 or newer, no dependencies, reads your manifest, asks the registry about every package, and exits non-zero if anything is missing.

// check-deps.mjs — run after an agent edits package.json, before npm install.
// Usage: node check-deps.mjs [path/to/package.json]
import { readFile } from "node:fs/promises";

const REGISTRY = "https://registry.npmjs.org";
const NEW_DAYS = 30;      // packages younger than this get a REVIEW flag
const MIN_VERSIONS = 3;   // fewer than this gets a REVIEW flag

const file = process.argv[2] || "package.json";
const pkg = JSON.parse(await readFile(file, "utf8"));
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
const names = Object.keys(deps);

if (names.length === 0) {
  console.log("no dependencies to check");
  process.exit(0);
}

let missing = 0;
for (const name of names) {
  let res;
  try {
    res = await fetch(`${REGISTRY}/${encodeURIComponent(name)}`);
  } catch (err) {
    // Fail closed: if you cannot reach the registry, you cannot verify.
    console.error(`UNVERIFIED  ${name}: ${err.message}`);
    process.exit(2);
  }
  if (res.status === 404) {
    console.log(`MISSING     ${name} - not in the registry. Do not install.`);
    missing += 1;
    continue;
  }
  if (!res.ok) {
    console.error(`UNVERIFIED  ${name}: registry returned HTTP ${res.status}`);
    process.exit(2);
  }
  const data = await res.json();
  const created = data.time && data.time.created;
  const ageDays = created
    ? Math.floor((Date.now() - new Date(created).getTime()) / 86400000)
    : null;
  const versions = data.versions ? Object.keys(data.versions).length : 0;
  const maintainers = Array.isArray(data.maintainers) ? data.maintainers.length : 0;
  const flags = [];
  if (ageDays !== null && ageDays < NEW_DAYS) flags.push(`new (${ageDays}d)`);
  if (versions < MIN_VERSIONS) flags.push(`few versions (${versions})`);
  if (maintainers <= 1) flags.push("single maintainer");
  const verdict = flags.length ? "REVIEW    " : "OK        ";
  console.log(
    `${verdict} ${name} - created ${created ? created.slice(0, 10) : "unknown"}, ` +
    `${versions} versions, ${maintainers} maintainers` +
    (flags.length ? `  [${flags.join("; ")}]` : "")
  );
}

if (missing) {
  console.log(`\n${missing} package(s) not found in the registry. Stop here.`);
  process.exit(1);
}
console.log("\nno missing packages. REVIEW flags are prompts to look, not verdicts.");

What the flags mean:

Run it right after you read the diff and before you install:

node check-deps.mjs

If you want it to be impossible to forget, put it in front of the dev server:

{ "scripts": { "dev": "node check-deps.mjs && next dev" } }

It makes network calls on every start, so keep it out of offline CI paths, or point CI at the lockfile instead.

The 30-second version

If a script is too much for the moment, the same facts are one command away per package:

npm view <package> time.created
npm view <package> versions --json
npm view <package> maintainers --json

A package created last week, with two versions and one maintainer, that the agent "found" — that is the profile. You are looking for a reason to trust it, not the absence of a reason to distrust it.

Two habits that prevent most of this

The script is a net. The habits are the fence.

First, ask the agent for the exact package name, the exact version, and the registry URL of every new dependency — in the same session, before you accept the diff. If the agent cannot produce a URL that resolves, you have your answer before any script runs. Most hallucinations die right there, because the agent has no way to fabricate a working registry URL.

Second, keep the first install boring. npm install --ignore-scripts for anything new — postinstall scripts run with your permissions, and a package that needs to execute code during install to be useful is telling you something. Then npm ci from the lockfile everywhere after that, so what you reviewed is what runs.

What this does not do

Existence is not legitimacy. A squatted name can be years old with dozens of versions. Age and version count raise the cost of the attack; they do not eliminate it.

A name that resolves is not necessarily the package you meant. The USENIX study found that 8.7% of hallucinated Python package names actually exist — in the npm registry (reported in the CSA note). If your agent writes Python, "it exists on npm" is not an answer.

The script reads your manifest. It does not scan node_modules, transitive dependencies, or anything the agent changed outside package.json. And it does not know what you asked the agent to do. That part is still you, reading the diff — which was true before agents and is more true now.

Sources

This post was written with AI assistance. The author is responsible for its content.