What goes wrong when an AI picks your packages
AI coding tools sometimes suggest packages that don’t exist, or names that copy a popular package, and attackers publish harmful code under those names. The fix is to look a package up on the registry before you install it: check that it exists, that it is the one you meant, who publishes it and for how long, and what it runs when it installs.
A package is code someone else wrote that your app downloads from a public registry, such as npm for JavaScript or PyPI for Python. AI coding tools pick packages for you, and coding agents can run the install command themselves.
Three things can go wrong with the name alone:
- The package doesn’t exist. The model wrote a name that sounds right, but nobody ever published it. Researchers call this a package hallucination.
- Someone registered a made-up name. An attacker publishes their own code under a name that models invent, and waits for people to install it. Seth Michael Larson of the Python Software Foundation named this slopsquatting, after typosquatting.
- The name copies a popular one. A package is published under a name close to a well-known package, in the hope that people mistype it or mix the two up. This is typosquatting.
Two more risks come with the install itself. Some packages run a script on your machine while they install. And without a lockfile, the next install can pull in a newer version than the one you checked.
Who runs into this
Anyone who lets an AI tool add packages to their app:
- you build with Claude Code, Codex, Antigravity or Cursor and let it run
npm installorpip install; - you copy an install command from a chat answer;
- your agent fixes a “cannot find module” error by adding a package;
- your server, or the service that builds and tests your code (CI), installs packages without a lockfile.
The problem
Models get package names wrong often. A study presented at USENIX Security 2025 had 16 code-writing models produce 576,000 code samples in Python and JavaScript. Of the 2.23 million packages named in that code, 440,445 (19.7%) did not exist, including 205,474 different made-up names. The average rate was at least 5.2% for commercial models and 21.7% for open-source models.
The same wrong names come back. When the researchers asked questions that had produced a made-up name again, 10 times each, 43% of those names came back in all 10 answers, and 58% came back more than once. So an attacker can ask the questions people ask, collect the names that repeat, and register them first.
Someone has tried it. In March 2024, a researcher at Lasso Security uploaded an empty Python package named huggingface-cli, a name AI models had made up in his tests. In about three months it got more than 30,000 downloads, and he found its install command in the README of a repository for Alibaba research. The package was empty. An attacker could have put anything in it.
Installing also runs code. An npm package can include preinstall, install and postinstall scripts, and until npm 12, released in July 2026, npm install ran them for every dependency by default. In 2025 the Shai-Hulud worm used a postinstall script to steal data, then used npm tokens it found to publish bad versions of other packages. OWASP reports it reached over 500 package versions before npm disrupted it.
The study tested models available in early 2024, such as GPT-4 Turbo and CodeLlama. Newer tools may make fewer of these mistakes. The checks below work the same whichever model wrote the install command.
How to check a package before you install it
Check before the install command runs. One command answers most of the questions: npm view <name> time.created repository.url maintainers scripts.
- It exists. If
npm viewanswers with a 404 error, no package has that name: your AI made it up or got it wrong. Don’t install something with a similar name to make the error go away. Find the real name in the official docs of the library or service you are using. - It is the one you meant. Compare the name, character by character, with the one in those docs. Look for a missing or doubled letter, a dash that became an underscore, or a different scope (the part before the slash, as in
@types/node). Don’t rely on spotting typos alone: of the made-up Python names the researchers measured, only 13.4% were one or two characters away from a real package. And 8.7% were real JavaScript packages, so a name that exists on the wrong registry proves nothing. - It has a history.
time.createdshows when the name was first published, andmaintainersshows who can publish it. A package that appeared last week deserves a much closer look than one published for years. Check the date of the newest version too: an old package can still get a bad new version, which is how Shai-Hulud spread. - It links to real source code.
repository.urlshould lead to a repository with real code and an owner you recognize. If the package page on npmjs.com shows a green check mark next to the version, it was published with provenance: a public record of the source commit and build that produced it. Provenance tells you where a package came from. npm’s docs say it does not guarantee the package has no malicious code. - People use it, for what that’s worth. npm’s downloads API,
api.npmjs.org/downloads/point/last-week/<name>, returns last week’s download count. Very few downloads is a warning. Many downloads proves little: the emptyhuggingface-clipackage passed 30,000. - You know what it runs when it installs.
scriptslists anypreinstall,installorpostinstallscript. Some well-known packages have one for a good reason: esbuild, for example, runs a postinstall script. What matters is that you know what the script does before it runs.
/*
* Check a package name your AI suggested before you install it.
*
* checkPackage() turns what the npm registry says about a name into a list
* of reasons to stop and look. An empty list means none of these warning
* signs showed up. It does not mean the package is safe.
*
* The facts come from two public npm endpoints (fetchPackageFacts below):
* https://registry.npmjs.org/<name> 404 if no such package
* https://api.npmjs.org/downloads/point/last-week/<name> { "downloads": 1234, ... }
* By hand, all but the download count are one command away:
* npm view <name> time.created repository.url maintainers scripts
*/
export interface PackageFacts {
created: Date // when the name was first published
latestPublished: Date // when the newest version was published
repositoryUrl?: string
installScripts: string[] // which of preinstall, install, postinstall it has
weeklyDownloads: number
deprecated?: string
}
// Starting points, not rules. OpenSSF Scorecard's "Maintained" check also waits 90 days.
export const LIMITS = { minAgeDays: 90, minVersionAgeDays: 7, minWeeklyDownloads: 1000 }
const DAY_MS = 24 * 60 * 60 * 1000
const INSTALL_HOOKS = ['preinstall', 'install', 'postinstall']
export function checkPackage(
name: string,
facts: PackageFacts | null, // null when the registry answered 404
knownNames: string[], // packages you already use, or the names the official docs give
now: Date,
limits = LIMITS,
): string[] {
const warnings: string[] = []
if (!facts) warnings.push(`"${name}" does not exist on the registry`)
if (!knownNames.includes(name)) {
for (const known of knownNames) {
const edits = distance(name, known)
if (edits === 1) warnings.push(`"${name}" is one character off "${known}"`)
if (edits === 2) warnings.push(`"${name}" is two characters off "${known}"`)
}
}
if (!facts) return warnings
const age = daysBetween(facts.created, now)
const versionAge = daysBetween(facts.latestPublished, now)
if (age < limits.minAgeDays) warnings.push(`first published ${age} days ago`)
else if (versionAge < limits.minVersionAgeDays) warnings.push(`newest version published ${versionAge} days ago`)
if (facts.weeklyDownloads < limits.minWeeklyDownloads) warnings.push(`only ${facts.weeklyDownloads} downloads last week`)
if (!facts.repositoryUrl) warnings.push('no source repository linked')
for (const hook of facts.installScripts) warnings.push(`runs a ${hook} script when installed`)
if (facts.deprecated) warnings.push(`newest version is deprecated: ${facts.deprecated}`)
return warnings
}
/** The parts of the registry's package document this check reads. */
export interface RegistryDoc {
'dist-tags'?: { latest?: string }
time: Record<string, string> // "created", "modified", and one entry per version
repository?: string | { url?: string }
versions?: Record<string, { scripts?: Record<string, string>; deprecated?: string }>
}
/** Returns null when no version can be installed, as for a removed package. */
export function factsFromRegistry(doc: RegistryDoc, weeklyDownloads: number): PackageFacts | null {
const latest = doc['dist-tags']?.latest
const version = latest ? doc.versions?.[latest] : undefined
if (!latest || !version) return null
return {
created: new Date(doc.time.created),
latestPublished: new Date(doc.time[latest]),
repositoryUrl: typeof doc.repository === 'string' ? doc.repository : doc.repository?.url,
installScripts: INSTALL_HOOKS.filter((hook) => version.scripts?.[hook]),
weeklyDownloads,
deprecated: version.deprecated,
}
}
/** The only part that uses the network, so the tests don't call it. */
export async function fetchPackageFacts(name: string): Promise<PackageFacts | null> {
const id = encodeURIComponent(name)
const doc = await fetch(`https://registry.npmjs.org/${id}`)
if (doc.status === 404) return null
if (!doc.ok) throw new Error(`The registry answered ${doc.status}`)
const counts = await fetch(`https://api.npmjs.org/downloads/point/last-week/${id}`)
const downloads = counts.ok ? ((await counts.json()) as { downloads: number }).downloads : 0
return factsFromRegistry((await doc.json()) as RegistryDoc, downloads)
}
function daysBetween(from: Date, to: Date): number {
return Math.floor((to.getTime() - from.getTime()) / DAY_MS)
}
/** Levenshtein distance: the fewest one-character edits that turn a into b. */
function distance(a: string, b: string): number {
let previous = Array.from({ length: b.length + 1 }, (_, j) => j)
for (let i = 1; i <= a.length; i++) {
const row = [i]
for (let j = 1; j <= b.length; j++) {
row[j] = Math.min(previous[j] + 1, row[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1))
}
previous = row
}
return previous[b.length]
}checkPackage takes the name your AI suggested, the facts the registry returned, and the names you already trust: the packages in your package.json, or the ones the official docs give. It returns warnings, not a verdict. The limits are starting points. 90 days is the age OpenSSF Scorecard’s “Maintained” check waits for; the others are judgment calls. A near match can also be a real, different package, so a look-alike warning means “look closer”, not “remove”.
Then protect what you install:
- Commit the lockfile and install from it.
package-lock.jsonrecords the exact version of every package in your tree, including the ones your packages pull in, with a hash of each one. In CI, usenpm ci: it fails ifpackage.jsonand the lockfile disagree, and it never rewrites either. Without a lockfile, a range like^1.4.0, which npm writes by default, lets an install take any newer 1.x version. - Keep install scripts off unless a package needs one. On npm 12, dependency install scripts are blocked unless the
allowScriptslist in yourpackage.jsonallows them.npm approve-scripts --allow-scripts-pendinglists the packages waiting, and settingstrict-allow-scriptsmakes a package with an unreviewed script fail the install instead of being skipped with a warning. On older npm, install in CI withnpm ci --ignore-scriptsand handle the few packages that need a build step on purpose. - Wait before taking brand-new versions. npm 12 has a
min-release-agesetting: it installs only versions published more than the number of days you choose. - Keep checking after the install.
npm auditasks the registry for known vulnerabilities in what you installed, and--audit-level=highmakes it fail on high-severity findings or worse.npm audit signatureschecks the registry signatures and provenance of what you installed. On GitHub, Dependabot alerts tell you when a new advisory matches one of your dependencies, and the dependency review action can fail a pull request that adds a vulnerable package.
Why it matters for AI-written code
When you type an install command yourself, you see the name before anything runs. A coding agent can pick the name, write the import and run the install in one step. Any install script has then run before you read the change. And if you only check that the app works, a new line in package.json is easy to miss.
Agents also add packages to fix errors. If the agent imported a package that doesn’t exist, the quick fix, installing a package with that name, is exactly what a slopsquatter is waiting for. The repeat rate in the study is what makes this work for an attacker: a name that comes back in every answer is worth registering.
Other ways to check, and how they compare
- Ask the AI if the package is real. In the study, several models could spot their own made-up names with more than 75% accuracy. That makes it a useful first question, not a check. The registry is the check.
- Go by downloads or stars. They show that people use a package, not who wrote it or what it does. The empty
huggingface-clipackage passed 30,000 downloads. - Rely on npm audit. It reports known vulnerabilities in the versions you installed. A new malicious package that nobody has reported yet passes it.
- Trust the registry to block fakes. npm says it can detect typosquats and block them from being published. A made-up name that isn’t close to any popular package doesn’t look like a typo, and in the study most made-up names weren’t close to a real one.
- OpenSSF Scorecard. It scores a project’s repository from 0 to 10 on checks such as whether it is maintained and whether code is reviewed. You can view a score at
scorecard.dev/viewer/?uri=github.com/<owner>/<repo>. It helps you choose between two real packages. Its docs call the checks heuristics, with false positives and false negatives, and it scores the repository, so first make sure the package really comes from it. - Dependabot malware alerts. GitHub can alert you when a dependency matches a known malware advisory, including for npm. You turn them on in Settings, Code security, Dependabot. GitHub notes that new malware may take time to appear in its advisory database, so this catches bad packages after they’re reported, not before you install them.
How to use it in your app
- Tell your coding agent to ask before it installs any new package. The text below does this.
- For every package it proposes, run
npm viewor the example above, and open the official docs page that names the package. - Commit
package-lock.jsonand switch CI fromnpm installtonpm ci. - On npm 12, keep install scripts blocked and approve only the packages that need a build step. On older npm, add
--ignore-scriptsto installs in CI. - Add
npm audit --audit-level=highto CI. On GitHub, turn on Dependabot alerts, malware alerts and dependency review. - For Python, ask PyPI the same questions:
pypi.org/pypi/<name>/jsonreturns each release’s upload time and the project’s links, andpip-audit -r requirements.txtchecks for known vulnerabilities the waynpm auditdoes.
What to tell your coding agent. Paste this into Claude Code, Codex or Antigravity:
Rule for this project: don’t install, add or import a package I haven’t approved. When you want a new one, stop and show me its exact name, a link to the official documentation page that tells you to use it, and the output of npm view <name> time.created dist-tags.latest maintainers repository.url scripts. Tell me plainly if the registry answers 404, if the package was first published less than 90 days ago, if its name is one or two characters away from a better-known package, or if it has a preinstall, install or postinstall script. Separately: make sure package-lock.json is committed and CI installs with npm ci, add npm audit --audit-level=high to CI, and list every dependency in package.json with one line on why we need it, marking any that nothing imports.
To make it a standing rule instead of a one-off request, put the same text in the instructions file your agent reads at the start of a session: CLAUDE.md for Claude Code, AGENTS.md for Codex, and AGENTS.md, GEMINI.md or a file in .agents/rules/ for Antigravity. ArcLab doesn’t connect to any of these tools; you paste the text yourself.
Learn it in depth
This guide covers the check before you install. The “Check AI-suggested packages before you install them” module in the Security Engineering track covers the rest: locking what you run, keeping install scripts under control and scanning after install. You decide what to do in a scenario where your agent has installed a package you don’t know, and you finish with a dependency intake policy written for your own app.
This guide fixes one problem. ArcLab covers 35.
$29 one-time for all 35 modules, plus the ones we add. Or start free: check your own app in about 15 minutes. 14-day refund, as long as you haven’t completed more than one module.
Sources
The official documentation this guide was checked against:
- We Have a Package for You! A Comprehensive Analysis of Package Hallucinations by Code Generating LLMs, USENIX Security 2025, Spracklen et al.: 16 models, 576,000 samples; 19.7% of 2.23 million packages made up; 5.2% and 21.7% averages; 43% and 58% repeats; 13.4% within two characters; 8.7% real JavaScript names; over 75% self-detection; models tested
- Diving Deeper into AI Package Hallucinations, Lasso Security, Bar Lanyado: the empty huggingface-cli package, more than 30,000 downloads in about three months, Alibaba README
- LLMs can’t stop making up software dependencies and sabotaging everything, The Register: Seth Michael Larson named slopsquatting
- Threats and mitigations, npm Docs: typosquatting; npm blocks some typosquats from being published
- A03:2025 Software Supply Chain Failures, OWASP Top 10:2025: Shai-Hulud: postinstall script, stolen npm tokens, over 500 package versions
- npm v12.0.0 release notes, npm/cli on GitHub: released 8 July 2026; dependency lifecycle scripts blocked by default unless allowScripts allows them
- Upcoming breaking changes for npm v12, GitHub Changelog: preinstall, install and postinstall from dependencies no longer run unless allowed
- npm approve-scripts, npm Docs (CLI v12): allowScripts in package.json; --allow-scripts-pending
- Config, npm Docs (CLI v12): ignore-scripts, strict-allow-scripts, min-release-age, save-prefix defaults to ^
- npm view, npm Docs (CLI v12)
- Package metadata, npm registry docs on GitHub: time.created and per-version times, repository, maintainers
- Download counts, npm registry docs on GitHub: the last-week endpoint
- Generating provenance statements, npm Docs: provenance does not guarantee the package has no malicious code
- Viewing package provenance, npm Docs: the green check mark; npm audit signatures
- package-lock.json, npm Docs (CLI v12): exact tree, integrity hashes, commit it
- npm ci, npm Docs (CLI v12): fails on a mismatch, never writes package.json or the lockfile
- About semantic versioning, npm Docs: what a ^ range allows
- npm audit, npm Docs (CLI v12): known vulnerabilities, --audit-level, audit signatures
- OpenSSF Scorecard, OpenSSF on GitHub: 0 to 10 scores, the web viewer, checks are heuristics
- Check documentation: Maintained, OpenSSF Scorecard on GitHub: only succeeds for projects more than 90 days old
- About Dependabot alerts, GitHub Docs: alerts when a new advisory matches a dependency
- Dependabot malware alerts, GitHub Docs: ecosystems covered; new malware may take time to appear
- Dependabot now detects malware in npm dependencies, GitHub Changelog: opt-in, where to turn it on
- About dependency review, GitHub Docs: the action fails on vulnerable packages
- JSON API, PyPI Docs: upload times and project URLs
- pip-audit, PyPA on GitHub: scans Python dependencies for known vulnerabilities
- How Claude remembers your project, Claude Code docs, Anthropic: CLAUDE.md is read at the start of every session
- AGENTS.md, Codex docs, OpenAI: Codex reads AGENTS.md before doing any work
- Rules, Google Antigravity docs: workspace rules in AGENTS.md, GEMINI.md or .agents/rules/
Last checked 23 October 2026 against the sources above.
Found a mistake? Tell us.