You ask Claude Code to add JWT auth to a Flask app. It decides it needs a helper library and runs:

pip install flask-jwt-securex

That package doesn't exist. The model made the name up. Today the install just fails. But if someone registers flask-jwt-securex on PyPI tomorrow, the same command installs their code on your machine, with your credentials in reach. This is called slopsquatting.

It isn't rare enough to ignore. Researchers generated 576,000 code samples with 16 models and found 205,474 unique package names that don't exist. On average, at least 5.2% of packages suggested by commercial models were made up, and 21.7% for open-source models (Spracklen et al., USENIX Security 2025).

One hook stops it

pip install skylos
skylos agent install-hooks --claude
Installed 9 Skylos hook(s) in .claude/settings.json (skylos 4.48.0).

One of those hooks runs before every Bash command the agent tries. It reads the command, finds any pip install or npm install, and checks each package against the public registry first.

What it blocked

We sent the hook the same JSON Claude Code sends before running a command:

The agent runsResult
pip install flask-jwt-securexBlocked: doesn't exist on PyPI
pip install reqeustsBlocked: doesn't exist on PyPI (a typo of requests)
npm install react-secure-jwt-helperzBlocked: doesn't exist on npm
npm install loadshBlocked: exists, but looks like a typosquat of lodash
pip install requestsAllowed
npm install zodAllowed

The loadsh case matters most. That package really exists on npm, so a check that only asks "does this name exist?" would let it through. Skylos flags it because the name is one letter-swap away from lodash.

Here's the reason the hook returns, as printed:

Skylos blocked this install: 1 package looks hallucinated or typosquatted.
- PyPI package 'flask-jwt-securex' does not exist on PyPI (likely hallucinated; an attacker can register it).
Check the real package name (docs, registry search) and retry. If the package is internal, install it with your private index/registry flag so Skylos skips the public-registry check. If this package is correct, add it to [tool.skylos] hooks_allow_packages in pyproject.toml.

What the agent sees

The hook answers with a deny decision. Claude Code's docs describe what happens next: it "reads the JSON decision, blocks the tool call, and shows Claude the reason" (Claude Code hooks).

So the install never runs, and the agent gets a clear instruction to check the real name. It doesn't just try the same command again.

It's not only installs

install-hooks adds 9 hooks in total. Besides checking installs, they block the agent from reading secret files and check each edit it makes. The same command works for Cursor (--cursor) and Codex (--codex).

Before you accept an agent's "done", skylos done checks the whole change: deleted or skipped tests, added secrets, removed security controls, and imports that don't exist (what one scan catches).

Limits

  • Only commands the agent runs are checked. pip install -r requirements.txt passes the hook. To catch a made-up name already written into a requirements file, run skylos . -a, which reports it as a hallucinated dependency.
  • It needs Skylos on the machine. The hook calls this machine's Skylos. On a machine without it, the hook does nothing and never blocks. Teammates run skylos agent install-hooks themselves.
  • It fails open on internal errors. If the check itself breaks, the command isn't blocked, but the hook says it wasn't checked.
  • It checks public registries. PyPI and npm here; internal packages need your private index flag or an allow-list entry.