Guide

Slopsquatting: Verify Every AI-Suggested Package Before You Install It

Check every AI-suggested npm or PyPI package before install: existence, publish age, downloads, source repo and install scripts, plus a CI gate.

2026-10-14

A dark conveyor carries package crates toward a lit vault, while one crate with an amber label sits under a glass inspection hood.
Check the package before it reaches the project, not after.

1. Why an AI-suggested name needs a check

Your assistant writes a working-looking script and ends with npm install or pip install for a package you have never heard of. Sometimes that package does not exist. If someone has registered the invented name in the meantime, the install command fetches their code instead. That attack is called slopsquatting: squatting on names that a model tends to make up.

A large empirical study comes from Spracklen and colleagues, "We Have a Package for You!", presented at USENIX Security 2025. They ran 16 code models on Python and JavaScript tasks and produced 576,000 code samples. Of 2.23 million package references in those samples, 440,445 (19.7%) named a package that was not on the PyPI or npm package lists the authors took on 10 January 2024. That came to 205,474 unique invented names. The average rate was at least 5.2% for the commercial GPT models they tested and 21.7% for the open-source models.

Repetition matters more than the rate. The authors re-ran 500 prompts that had produced a hallucinated name, 10 times each, using Python and four of the models. In 43% of cases the same invented name came back in all 10 runs, and in 39% it never came back. A name that reappears that reliably is worth registering for an attacker. These are figures for the models in the study, not measured rates for the assistant you use today.

The paper also explains why existence alone is a weak filter: once an attacker publishes the name, it exists. So the check below has five parts, and none of them is a verdict on its own. It reduces risk; it does not certify a package as safe.

2. Run the five-point check on one npm package

Do this before the name reaches package.json. One npm view call reads registry metadata without installing anything. The documented dot notation selects fields and --json prints them as JSON:

npm view <name> name time.created repository.url scripts --json
curl -s https://api.npmjs.org/downloads/point/last-week/<name>

The second line uses npm's download counts API. Read the five answers in this order:

CheckWhere to read itStop and investigate when
1. Existencenpm view exit statusE404: the name is not on the registry. Do not install; find the real package.
2. Publish agetime.createdThe package is younger than your policy allows, for example 30 days.
3. Downloadsdownloads for last-weekThe count is tiny compared with what the assistant implied ("the standard library for this").
4. Source repositoryrepository.urlThe field is missing, the repository does not exist, or it belongs to a different project.
5. Install scriptsscriptspreinstall, install or postinstall is present and you cannot explain it from the repository.

Run the check on a package you know before you trust your reading of it. On 14 October 2026, with npm 11.6.2, express showed a creation date of 29 December 2010, a GitHub repository, and scripts for linting and tests but no preinstall, install or postinstall entry. esbuild showed "postinstall": "node install.js". That is a legitimate, widely used package with an install script, so the fifth check means "read the script and its repository", not "reject on sight".

If you want to install a candidate to inspect it, npm's ignore-scripts setting stops package scripts from running during install. It does not make the code safe to import later, which section 7 returns to.

3. The same check for a PyPI package

PyPI's JSON API serves project metadata at /pypi/<project>/json. Save the response, check the status code, then read the source link and the oldest upload time:

curl -s -o pkg.json -w "%{http_code}\n" https://pypi.org/pypi/<name>/json
python -c "import json; d=json.load(open('pkg.json')); print(d['info']['project_urls']); print(min(f['upload_time_iso_8601'] for r in d['releases'].values() for f in r))"

Two limits apply. PyPI documents the releases key on this endpoint as deprecated and possibly removed later, so treat the one-liner as a quick check rather than a tool to build on. The documentation's example also shows the download fields as -1, so this API gives you no download signal; judge age, release history and the linked source instead. In the test run, requests returned status 200, a Source link to github.com/psf/requests and a first upload in February 2011. This guide reads install scripts from npm metadata only; for a Python package, reviewing the linked repository is that step.

4. Make the lockfile and an allowlist the rule

A one-off check helps once. A rule keeps working when the assistant suggests something new next week. Use two files in the repository:

The trade-off is friction: every new dependency now needs a second person. That is the intended cost. The Cloud Security Alliance's research note of 11 August 2026 recommends a similar pair of controls: flag packages that are new (it suggests 30 to 90 days) and restrict AI coding agents to an approved allowlist. If an agent can run installs on its own, give it the allowlist too; Sandbox an AI Coding Agent in a Dev Container covers limiting what such an agent can reach.

5. Add a CI gate that blocks unreviewed names

The gate compares the dependency names in the pull request with the names on the base branch. Any added name that is missing from the allowlist fails the job. Save this as scripts/check-new-deps.mjs; it needs only Node.js and reads either package.json or requirements.txt:

// Usage: node check-new-deps.mjs <base-file> <head-file> <allowlist-file>
// Files are package.json or requirements.txt. Exit 1 if the PR adds
// a dependency name that is not on the reviewed allowlist.
import { readFileSync } from "node:fs";

const [base, head, allowFile] = process.argv.slice(2);

function names(file) {
  const text = readFileSync(file, "utf8");
  if (file.endsWith(".json")) {
    const pkg = JSON.parse(text);
    return new Set(["dependencies", "devDependencies", "optionalDependencies"]
      .flatMap((key) => Object.keys(pkg[key] ?? {})));
  }
  const result = new Set();
  for (const raw of text.split(/\r?\n/)) {
    const line = raw.replace(/#.*/, "").trim();
    if (!line) continue;
    // Accept only simple name==version pins. Reject includes, URLs,
    // editable installs, markers and other unsupported syntax.
    const match = line.match(/^([A-Za-z0-9][A-Za-z0-9._-]*)==([A-Za-z0-9][A-Za-z0-9.!+_-]*)$/);
    if (!match) throw new Error("Unsupported requirement; review manually: " + line);
    result.add(match[1].toLowerCase().replace(/[-_.]+/g, "-"));
  }
  return result;
}

if (!base || !head || !allowFile) throw new Error("Provide base, head and allowlist files.");
const normalize = head.endsWith(".json") ? (name) => name
  : (name) => name.toLowerCase().replace(/[-_.]+/g, "-");
const allowed = new Set(readFileSync(allowFile, "utf8").split(/\r?\n/)
  .map((line) => normalize(line.trim())).filter(Boolean));
const before = names(base);
const added = [...names(head)].filter((name) => !before.has(name));
const unreviewed = added.filter((name) => !allowed.has(name));

for (const name of added) {
  console.log(`${unreviewed.includes(name) ? "BLOCK" : "ok   "} ${name}`);
}
if (unreviewed.length) {
  console.error(`${unreviewed.length} new dependency name(s) need review before merge.`);
  process.exit(1);
}
console.log("No unreviewed new dependencies.");

In the CI job, make sure the base branch has been fetched, write its file next to the checked-out one and run the script:

git show origin/main:package.json > base-package.json
node scripts/check-new-deps.mjs base-package.json package.json approved-dependencies.txt

The gate checks names, not versions, and it only sees direct dependencies in the file you pass. A transitive dependency that arrives through an approved package needs the lockfile review from section 4. For Python, this small example accepts only simple name==version pins and normalizes case and runs of dots, underscores and hyphens. Includes such as -r other.txt, URLs, editable installs and other syntax stop the job for manual review. It is not a general requirements parser. Missing input files also stop the job. Keep the checker and allowlist under trusted review: if a pull request can change either without a separate approval, it can bypass the rule.

6. Prove it with a made-up package name

Use a name that should not exist: apg-slopcheck-probe-q7x2. Run the single-package check, then add the name to a branch's package.json and run the gate with an allowlist that contains only your existing dependencies. These are the results recorded on 14 October 2026:

TestExpected and observed result
npm view apg-slopcheck-probe-q7x2 namenpm error code E404, exit code 1
PyPI JSON for the same nameHTTP 404
Gate, name added, not on allowlistBLOCK apg-slopcheck-probe-q7x2, exit code 1
Gate, same name added to the allowlistok, exit code 0
Gate, name added to requirements.txt with an exact == version pin and commentBLOCK, exit code 1

The 404 responses are the registries' observed behavior in that run, not a documented promise. The fourth row is the important one: the gate passes as soon as someone approves the name. If the probe name ever starts returning metadata, someone has registered it, and that is exactly the situation the human review must catch.

7. What the checks do not cover

The CSA note describes reporting on an npm campaign in 2026 whose packages avoided preinstall and postinstall hooks and instead told developers to load the code with require(). CSA treats the claim that its names came from AI hallucinations as unconfirmed. Either way, a clean scripts field would not have caught it, which is why the allowlist review asks whether you need the package at all, not only how it installs.

Age, downloads and a repository link can all be arranged by a patient attacker. Use the five answers to decide where to look harder. For upgrading packages you already depend on, follow The AI Dependency Upgrade Plan. For the publishing side of the same supply chain, see npm Adds Multiple Trusted Publishers.

Sources checked 2026-10-14