
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:
| Check | Where to read it | Stop and investigate when |
|---|---|---|
| 1. Existence | npm view exit status | E404: the name is not on the registry. Do not install; find the real package. |
| 2. Publish age | time.created | The package is younger than your policy allows, for example 30 days. |
| 3. Downloads | downloads for last-week | The count is tiny compared with what the assistant implied ("the standard library for this"). |
| 4. Source repository | repository.url | The field is missing, the repository does not exist, or it belongs to a different project. |
| 5. Install scripts | scripts | preinstall, 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:
- An npm lockfile (
package-lock.json) records the resolved dependency tree. Exact version pins inrequirements.txtconstrain the Python packages listed there, but do not by themselves lock unlisted transitive dependencies or verify file hashes. Commit the dependency file and review any pull request that adds a new top-level entry. approved-dependencies.txtlists one reviewed package name per line. A name gets onto the list only after someone has run the five-point check and recorded the result in the pull request.
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:
| Test | Expected and observed result |
|---|---|
npm view apg-slopcheck-probe-q7x2 name | npm error code E404, exit code 1 |
| PyPI JSON for the same name | HTTP 404 |
| Gate, name added, not on allowlist | BLOCK apg-slopcheck-probe-q7x2, exit code 1 |
| Gate, same name added to the allowlist | ok, exit code 0 |
Gate, name added to requirements.txt with an exact == version pin and comment | BLOCK, 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
- Spracklen et al., "We Have a Package for You!" (USENIX Security 2025) — sample size, hallucination rates, unique names, the 10-run repeat test and the limit of existence filtering.
- npm Docs: npm view — field selection,
time,repository.urland--json. - npm Docs: config —
ignore-scripts. - npm registry: download counts — the
last-weekpoint endpoint. - PyPI Docs: JSON API — project endpoint, upload times, project URLs, deprecated
releasesand download fields. - Cloud Security Alliance research note, 2026-08-11 — campaign background and the new-package and allowlist recommendations.