The Package Your AI Assistant Recommended Does Not Exist

Models invent plausible package names. Attackers register them. Here is why this attack works and the two checks that stop it.

Share
The Package Your AI Assistant Recommended Does Not Exist. Abstract ai tooling illustration in orange and dark grey on debugly.dev

The short answer

Coding assistants regularly suggest packages that do not exist. The names are plausible because they are constructed from patterns in real package names.

The security problem: attackers watch for commonly hallucinated names and register them. When the next developer runs the install command, they get the attacker's code. This has been given the name slopsquatting, and it is a variant of typosquatting where the model rather than a typo generates the wrong name.

Two checks stop it:

npm view <package>              # does it exist, and since when
npm view <package> time.created # a brand new package for an old-sounding name is a red flag

And structurally: use a lockfile, and never let an agent install dependencies without review.

Why models invent packages

Package names are compositional. requests-oauth, django-cors-headers, express-rate-limit, react-use-debounce. Given that pattern, a model can generate express-jwt-refresh or react-use-clipboard-async with complete fluency, and there is nothing in the name to indicate whether it exists.

The model is doing what it does everywhere else: producing the most plausible continuation. For prose that is usually fine. For an identifier that resolves to executable code, plausible is not the same as correct, and the gap is where the attack lives.

This gets worse for smaller ecosystems and newer libraries, where the model has seen fewer real names and is extrapolating more.

Why it is a real attack rather than an annoyance

The failure mode is not that the install fails. If the package does not exist, npm install errors and you notice immediately. That is the good case.

The bad case:

  1. A model suggests @corp/logger-utils in answer to a common question
  2. It suggests the same name to many people, because the same question produces similar completions
  3. Somebody registers that name with a package that runs a postinstall script
  4. The next developer installs it without checking

Package managers execute code at install time by default. npm runs postinstall. Python packages run setup.py. That means installing a package is running untrusted code on the machine that has your SSH keys, your cloud credentials, and your git access.

The blast radius is worse in CI, where the credentials are usually broader and nobody is watching the output.

The two checks

Before installing anything you did not personally choose:

# npm
npm view express-rate-limit
npm view express-rate-limit time.created
npm view express-rate-limit maintainers

# pypi
pip index versions requests
curl -s https://pypi.org/pypi/requests/json | jq '.info.author, .urls[0].upload_time'

Three signals worth reading:

Age. A package created last week, with a name that sounds like it has been around for years, is suspicious. Real widely used libraries have history.

Download counts. A package the model presented as standard should have meaningful weekly downloads. Fifty downloads for something described as popular is wrong.

Repository link. Does it point at a real repository with real commits and real issues? A package with no repository, or a repository created the same week, deserves scrutiny.

None of these are conclusive alone. Together they are quick and they catch the obvious cases, which is most of them.

Structural defences

The checks above depend on somebody remembering. These do not.

Lockfiles, committed, with frozen installs in CI.

npm ci                              # not npm install
pnpm install --frozen-lockfile
pip install -r requirements.txt --require-hashes
uv sync --frozen

A lockfile pins the exact version and, in most formats, an integrity hash. npm ci fails if the lockfile and manifest disagree, which means a new dependency cannot appear in CI without a visible change to a file somebody reviewed.

This is the single most effective control, and it costs nothing.

Disable install scripts where you can.

npm config set ignore-scripts true

This breaks packages that genuinely need to compile native code, so it is not universally applicable. But it converts "installing runs arbitrary code" into "installing copies files", which is a large reduction in exposure. Where you need scripts for a specific package, allow it explicitly.

Review the dependency diff in every pull request. A change to package.json or requirements.txt should get more attention than a change to application code, not less. In practice it usually gets less, because it is one line and looks boring.

I would treat a new dependency as requiring the same scrutiny as a new service integration. Someone should be able to say why this package, why not the alternative, and who maintains it.

Use a private registry or proxy with an allowlist if you are in an organisation with the appetite for it. Artifactory, Verdaccio, or your cloud provider's artifact registry can mediate what gets in.

Pin transitive dependencies too. The package you reviewed may depend on something you did not.

The agent workflow question

If you run coding agents that can execute commands, this connects directly to how you sandbox them.

An agent that can run npm install unattended can install anything, including something it invented thirty seconds earlier. The controls that matter:

No unattended installs. Dependency changes should require human approval even in an otherwise autonomous loop. This is a small friction cost against a large downside.

Restricted egress. An agent whose network access is limited to your registry and your git host cannot fetch from an arbitrary domain even if a postinstall script tries.

Isolated environment. An agent running in a container or a microVM with scoped credentials means a malicious postinstall gets a throwaway environment rather than your laptop.

Log the commands. When something goes wrong you need to know exactly what was installed and when.

When the package genuinely does not exist

Worth saying: most of the time the hallucinated package is simply absent and you get an error. The right response is not to search for something similar and install that, which is exactly the behaviour the attack relies on.

The right response is to go back to the actual problem. The model suggested a package because it was reaching for a capability. Find the real library that provides it, from documentation or from a maintained awesome-list, or write the twenty lines yourself.

I have noticed the temptation to install a near miss is strongest when you are tired and the suggested name looks almost right. That is precisely when the check is worth thirty seconds.

A pre commit check

If you want something mechanical, a simple script that flags newly added dependencies below a download threshold or above a recency threshold catches most of this:

#!/usr/bin/env bash
# flag dependencies added in this diff that look new or unpopular
git diff --cached -U0 package.json \
  | grep -oP '^\+\s+"\K[^"]+(?=":)' \
  | while read -r pkg; do
      created=$(npm view "$pkg" time.created 2>/dev/null)
      downloads=$(npm view "$pkg" --json 2>/dev/null | jq -r '.dist.tarball' >/dev/null && \
                  curl -s "https://api.npmjs.org/downloads/point/last-week/$pkg" | jq -r '.downloads')
      echo "$pkg  created=$created  weekly=$downloads"
    done

Rough, and it prints exactly the two numbers a reviewer needs to make a judgement. Good enough to catch the case this post is about.

Related: the six bugs coding agents write most often, where invented APIs are the first category, and the same underlying cause produces both.