To stop a coding agent installing a package it invented, put three checks in front of every install: a pre-install hook that blocks names missing from npm or PyPI or first published less than 30 days ago, a release-age gate in the package manager, and a rule that makes installs ask first. I tested the setup on 12 October 2026 with Claude Code 2.1.296, pnpm 10.34.3 and uv 0.9.21. The risk is measured: in a USENIX Security 2025 study, 19.7% of 2.23 million package references written by 16 models named packages that do not exist.
What is slopsquatting, and how often do agents invent packages? #
Slopsquatting is registering a package name that language models hallucinate, so that anyone who runs the model's install command gets the attacker's code. Seth Larson of the Python Software Foundation coined the term, according to Socket's April 2025 post. It works because models repeat themselves: when Spracklen and colleagues reran prompts ten times, 43% of hallucinated names came back every time.
Frontier models still do it. A 2026 preprint measured five at 4.62% to 6.10%, close to the 5.2% the 2024 study found for commercial models, and all five invented the same 127 names. On 6 October, a dev.to test of 30 npm prompts counted 6 fake suggestions: 2 from Claude Haiku 4.5, 1 from GitHub Copilot and 3 from ChatGPT and OpenAI Codex. The author did not record the total number of suggestions, so no rate follows from it.
Squatted names do get installed. Lasso Security's empty huggingface-cli package on PyPI drew more than 30,000 downloads in three months in a 2024 experiment. The hallucinated npm name react-codeshift spread to 237 repositories before an Aikido researcher registered it defensively in January 2026.
Coding agents pick packages from memory #
A coding agent picks a library from what its model learned, and checks the registry only if something makes it. In Amplifying's February 2026 study of 2,430 Claude Code responses on the same repositories, Prisma's share of picks in its category went from 79% under Claude Sonnet 4.5 to 0% under Claude Opus 4.6, replaced by Drizzle. Only the model changed. Amplifying sells vendors dashboards of these picks.
Claude Code asks before it searches the web, and Codex answers from a cached search index by default, so neither looks a name up unprompted. agent.reviews, launched on 7 October by Armature (Y Combinator, Spring 2026), adds reviews agents write right after a task, such as Prisma demanding a DATABASE_URL before any database exists. Its 50,000 measured agent sessions come with no published method, Armature sells "get your product picked by coding agents", and an invented name has no reviews to read.
Semantic versioning does not help either: a release "can preserve every exported TypeScript type and still change what users observe", Raju Dandigam wrote on 2 October, and a version number says nothing about who published it. On Stackness, as of 12 October 2026, 8 real profiles list Claude Code and 7 list Cursor, while 1 lists npm, 2 list pnpm and 1 lists uv (data sources). The numbers are small, and few stacks list the package manager the agent drives. The agents sit in the AI coding tools developers list.
What you need #
The guardrails need an agent that runs a script before each shell command, Node.js for that script, and a package manager recent enough to refuse new releases. Older versions lack either the hook setting that blocks on a crash or the release-age key.
- A coding agent with hooks: Claude Code 2.1.295 or later for
onFailure,Cursor 1.7 or later, or Codex 0.124 or later - Node.js 18 or later for the hook script, which uses the built-in
fetch - One package manager with a release-age setting: npm 11.10, pnpm 10.16, Yarn 4.10,Bun 1.3, uv 0.9.17 orpip 26.1, or later
1. Block missing and new names with a pre-install hook #
A pre-install hook runs before the agent's shell command and can stop it. The script below reads the command, finds the names in an npm, pnpm, Yarn, Bun, pip, uv or Poetry install, and blocks the call when a name is missing from the registry or was first published less than 30 days ago. Save it as .claude/hooks/check-package.mjs:
const MIN_AGE_DAYS = 30;
const input = JSON.parse(await new Promise((r) => {
let s = "";
process.stdin.on("data", (c) => (s += c)).on("end", () => r(s));
}));
const cmd = input.tool_input?.command ?? input.command ?? "";
const npm = /\b(?:npm|pnpm|yarn|bun)\s+(?:install|i|add)\s+([^;&|]+)/.exec(cmd);
const pip = /\b(?:pip3?\s+install|uv\s+add|uv\s+pip\s+install|poetry\s+add)\s+([^;&|]+)/.exec(cmd);
const names = (m) => (m ? m[1].trim().split(/\s+/) : [])
.filter((a, i, all) => !a.startsWith("-") && !/^(-r|-c|-e|--requirement|--constraint)$/.test(all[i - 1]))
.filter((a) => !/^[.\/~]|:/.test(a));
const ageDays = (iso) => (Date.now() - Date.parse(iso)) / 864e5;
const problems = [];
for (const arg of names(npm)) {
const name = arg.replace(/(?<=.)@[^/]*$/, "");
const res = await fetch(`https://registry.npmjs.org/${name.replace("/", "%2F")}`);
if (res.status === 404) { problems.push(`${name} does not exist on npm`); continue; }
const age = ageDays((await res.json()).time.created);
if (age < MIN_AGE_DAYS) problems.push(`${name} was first published ${age.toFixed(1)} days ago`);
}
for (const arg of names(pip)) {
const name = arg.split(/[=<>~!\[;]/)[0];
const res = await fetch(`https://pypi.org/pypi/${name}/json`);
if (res.status === 404) { problems.push(`${name} does not exist on PyPI`); continue; }
const uploads = Object.values((await res.json()).releases).flat().map((f) => f.upload_time_iso_8601);
const age = ageDays(uploads.sort()[0]);
if (age < MIN_AGE_DAYS) problems.push(`${name} was first published ${age.toFixed(1)} days ago`);
}
if (problems.length) {
console.error(`Blocked: ${problems.join("; ")}. Do not swap in a similar name. Ask the user before installing.`);
process.exit(2);
}
Register it in .claude/settings.json. Exit code 2 stops the tool call before permission rules run, and the agent reads the message. onFailure: "block" makes a crash or timeout block too:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-package.mjs",
"timeout": 30,
"onFailure": "block"
}
]
}
]
}
}
Asked to run pip install colourama, Claude Code 2.1.296 answered: "The command didn't run: a PreToolUse hook blocked it because colourama doesn't exist on PyPI... Did you mean colorama?" The line telling the agent not to swap names is there because a blocked agent otherwise tries the next guess.
2. Set a release-age gate in the package manager #
A release-age gate makes the package manager refuse any version published more recently than a set age, seven days in the table below. A name registered this week has only new versions, so its install fails with no version found. Pick the key for your package manager:
| Package manager | File | Setting for 7 days | Since |
|---|---|---|---|
| npm | .npmrc |
min-release-age=7 (days) |
11.10.0 |
| pnpm | pnpm-workspace.yaml |
minimumReleaseAge: 10080 (minutes) |
10.16.0 |
| Yarn | .yarnrc.yml |
npmMinimalAgeGate: "7d" |
4.10.0 |
| Bun | bunfig.toml |
[install]`` minimumReleaseAge = 604800 (seconds) |
1.3 |
| uv | pyproject.toml |
[tool.uv]`` exclude-newer = "7 days" |
0.9.17 |
| pip | command line | --uploaded-prior-to P7D |
26.1 |
The versions come from each project's changelog. With minimumReleaseAge set, pnpm 10.34.3 refused a 13-day-old package with a 14-day gate:
$ pnpm add solvatra-ai
ERR_PNPM_NO_MATCHING_VERSION No matching version found for solvatra-ai@latest
The latest release of solvatra-ai is "0.7.3". Published at 10/10/2026
npm 11.12.1 with --min-release-age=1 fails with ETARGET ... No matching version found for typescript@7.1.0-dev.20261012.1 with a date before 10/11/2026. The gate checks when each version was published, not when the name was first registered, so the hook's first-publish check covers the other half.
3. Make installs ask in every agent #
An ask rule turns every install into a question for you, which catches what the hook lets through. In Claude Code, add the install commands to permissions.ask in the same settings file. Ask rules apply before allow rules, so an allowed Bash(*) does not skip them:
{
"permissions": {
"ask": [
"Bash(npm install *)", "Bash(npm i *)", "Bash(npx *)",
"Bash(pnpm add *)", "Bash(pnpm dlx *)", "Bash(yarn add *)", "Bash(bun add *)",
"Bash(pip install *)", "Bash(uv add *)", "Bash(uvx *)"
]
}
}
OpenAI Codex keeps network access off in its default workspace-write sandbox, so an install already waits for approval. Leave network_access = true out of config.toml, and point a [[hooks.PreToolUse]] entry with matcher = "^Bash$" at the same script. In Cursor, register the script under beforeShellExecution in .cursor/hooks.json with "failClosed": true. The script reads Cursor's top-level command field as well. Cursor's default sandbox allows registry.npmjs.org and pypi.org, so without the hook a sandboxed install runs without asking.
4. Install from the lockfile and keep install scripts off #
A lockfile install fails instead of resolving anything new, so CI never installs a name the agent added without a reviewed lockfile change. Use npm ci, pnpm install --frozen-lockfile or uv sync --locked in CI, and read the lockfile diff in every agent pull request.
Install scripts are where a squatted package runs code during install. pnpm 10 and npm 12 block dependency lifecycle scripts by default; on npm 11, add ignore-scripts=true to .npmrc. That limits what a bad package does during install, not after your code imports it.
Check that it worked #
The hook works when a fake install prints the block message and exits with code 2, a real install passes with code 0, and the agent reports a denied tool call instead of running the command. Pipe a fake install into the script first:
echo '{"tool_input":{"command":"npm install react-codeshift-utils-pro"}}' \
| node .claude/hooks/check-package.mjs; echo "exit=$?"
Then ask the agent to run the same install. Claude Code reports a denied tool call and asks you instead. A real package such as npm i -D vitest passes with exit 0.
What goes wrong #
Five failures undo these guardrails against hallucinated packages: a hook that fails open, an old typosquat, private packages that look missing, commands that run packages without installing them, and a pnpm setting in the wrong file. Each has a short fix.
The hook fails open
A missing or crashing hook script lets the command run. On Claude Code 2.1.296, with the script path wrong, echo hook-test-ok ran; with "onFailure": "block" the same call was blocked. Cursor's equivalent is "failClosed": true, and Codex's hooks page says an error or timeout does not block the tool.
An old typosquat passes every check
expresss has existed since 2016, so it passes both the existence and the age checks. Put a malware feed in front of the registry for that case: Socket Firewall (npm i -g sfw, then sfw npm install <pkg>) blocks confirmed malicious packages without an account, and Aikido Safe Chain adds a 48-hour minimum age by default. Both are vendor products.
Private packages look missing
An internal package is missing from the public registry and fails the hook. Skip your scope in the script, and use the gate's exclude list: minimumReleaseAgeExclude in pnpm, min-release-age-exclude[] in npm 12, npmPreapprovedPackages in Yarn.
npx and sh -c run past the rules
npx some-cli downloads and runs a package without an install command, and Claude Code's permission docs say a rule "isn't a security boundary" against sh -c or a full binary path. Keep npx, pnpm dlx and uvx on the ask list, and turn on the agent's sandbox with an empty network allowlist when you want a hard stop.
pnpm ignores the gate
pnpm 11 and later read only auth and registry settings from .npmrc, so minimumReleaseAge belongs in pnpm-workspace.yaml. pnpm 11 also ships a one-day default that does not block an exact version unless you set the key yourself.
Checklist #
The checklist for stopping hallucinated installs covers the hook, the release-age gate, the ask rules, the lockfile, install scripts and a malware feed. Copy it into the repository's agent instructions or a pull request template:
[ ] .claude/hooks/check-package.mjs in the repo, registered as a PreToolUse hook with onFailure: block
[ ] The same script under beforeShellExecution in Cursor (failClosed) or as a Codex PreToolUse hook
[ ] A 7-day release-age gate in .npmrc, pnpm-workspace.yaml, .yarnrc.yml, bunfig.toml or pyproject.toml
[ ] Installs, npx, dlx and uvx on the agent's ask list
[ ] npm ci, pnpm install --frozen-lockfile or uv sync --locked in CI
[ ] Dependency install scripts off
[ ] A malware feed such as sfw for typosquats older than 30 days
This is the install step of agentic coding: the agent proposes the dependency, and a check and a person approve it.
Tools in this post #
Use any of these tools? #
Put them on a Stackness profile, say how you use each one and see who pairs them the same way. It takes a couple of minutes.