How to stop coding agents installing hallucinated packages: a pre-install hook, a release-age gate and an ask rule A three-part install guard — a pre-install hook blocking names absent from npm or PyPI or first published less than 30 days ago, a release-age gate in the package manager, and an ask-first rule — stops coding agents from installing hallucinated packages, tested on 12 October 2026 with Claude Code 2.1.296, pnpm 10.34.3 and uv 0.9.21. The risk is quantified: a USENIX Security 2025 study found 19.7% of 2.23 million package references written by 16 models named packages that do not exist, and Spracklen and colleagues found 43% of hallucinated names recurred across ten reruns of the same prompts. Lasso Security's empty huggingface-cli package on PyPI drew more than 30,000 downloads in three months in a 2024 experiment, while the hallucinated npm name react-codeshift spread to 237 repositories before an Aikido researcher registered it defensively in January 2026. How to stop coding agents installing hallucinated packages: a pre-install hook, a release-age gate and an ask rule 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 https://stackness.dev/tools/claude-code 2.1.296, pnpm https://stackness.dev/tools/pnpm 10.34.3 and uv https://stackness.dev/tools/uv 0.9.21. The risk is measured: in a USENIX Security 2025 study https://arxiv.org/abs/2406.10279 , 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 https://socket.dev/blog/slopsquatting-how-ai-hallucinations-are-fueling-a-new-class-of-supply-chain-attacks . 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 https://arxiv.org/abs/2605.17062 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 https://dev.to/harsh2644/i-tested-3-ai-coding-tools-for-slopsquatting-heres-how-many-fake-packages-they-invented-76b of 30 npm prompts counted 6 fake suggestions: 2 from Claude Haiku https://stackness.dev/tools/claude-haiku 4.5, 1 from GitHub Copilot https://stackness.dev/tools/github-copilot and 3 from ChatGPT https://stackness.dev/tools/chatgpt and OpenAI Codex https://stackness.dev/tools/codex-cli . 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 https://www.lasso.security/blog/ai-package-hallucinations . The hallucinated npm name react-codeshift spread to 237 repositories before an Aikido researcher registered it defensively https://www.infoworld.com/article/4167479/supply-chain-attacks-take-aim-at-your-ai-coding-agents-2.html 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 https://amplifying.ai/research/claude-code-picks of 2,430 Claude Code responses on the same repositories, Prisma https://stackness.dev/tools/prisma 's share of picks in its category went from 79% under Claude Sonnet https://stackness.dev/tools/claude-sonnet 4.5 to 0% under Claude Opus https://stackness.dev/tools/claude-opus 4.6, replaced by Drizzle https://stackness.dev/tools/drizzle-orm . 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 https://stackness.dev/tools/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 https://dev.to/raju dandigam/semantic-versioning-is-not-enough-for-ai-agent-libraries-2faj , 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 https://stackness.dev/tools/cursor , while 1 lists npm https://stackness.dev/tools/npm , 2 list pnpm and 1 lists uv data sources https://stackness.dev/about/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 https://stackness.dev/categories/ai-tools . 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 https://cursor.com/docs/hooks 1.7 or later, or Codex 0.124 or later - Node.js https://stackness.dev/tools/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 https://stackness.dev/tools/yarn 4.10, Bun https://stackness.dev/tools/bun 1.3, uv 0.9.17 or pip https://stackness.dev/tools/pip 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 : js 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 https://code.claude.com/docs/en/hooks 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: bash $ 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 https://learn.chatgpt.com/docs/agent-approvals-security 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 https://cursor.com/docs/reference/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=$?" Blocked: react-codeshift-utils-pro does not exist on npm. Do not swap in a similar name. Ask the user before installing. exit=2 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 https://learn.chatgpt.com/docs/hooks 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 https://stackness.dev/tools/socket-firewall npm i -g sfw , then sfw npm install