Run skylos . --select SKY-D222,SKY-D223 --strict: it looks up imported Python packages on PyPI and package.json dependencies on npm, and exits 1 when one doesn't exist or isn't declared. To stop an agent installing a made-up package in the first place, add the agent hooks.
AI coding tools name things that don't exist: packages, local helper functions, methods on real libraries. These look correct at a glance. A made-up package name is also a supply-chain risk, because anyone can register it (slopsquatting).
What it finds, on a real fixture
The test fixture has a pyproject.toml that declares requests==2.32.3, a package.json that declares express and one made-up package, and this module:
# app/client.py
import requests
import yaml
from cryptoutils import secure_hash
from flask_secured import require
from app import security # app/security.py defines authenticate() only
def fetch(url, request):
security.require_auth(request)
data = requests.fetch_json(url)
return secure_hash(data), yaml.safe_load("a: 1"), require
skylos . --ai-defects --format concise printed (dead-code lines omitted):
app/client.py:3 SKY-D222 Hallucinated dependency 'cryptoutils'. Package does not exist on PyPI.
app/client.py:4 SKY-D222 Hallucinated dependency 'flask_secured'. Package does not exist on PyPI.
app/client.py:2 SKY-D223 Undeclared import 'yaml' (provided by: pyyaml). Add to requirements.txt/pyproject.toml/setup.py.
app/client.py:11 SKY-D224 Installed package 'requests' does not expose callable API 'requests.fetch_json'.
web/package.json:1 SKY-D222 Hallucinated npm dependency 'express-secure-session-helperx'. Package does not exist in the npm registry.
app/client.py:10 SKY-L012 Call to 'security.require_auth()' resolves to local module 'app.security', but 'require_auth' is not defined or re-exported there. AI-generated code often hallucinates security helpers on local modules.
| Rule | What it means | Evidence it needs |
|---|---|---|
| SKY-D222 | The package doesn't exist on PyPI (imports) or npm (package.json) | A registry lookup, and for Python imports, proof that no declared dependency ships that module name |
| SKY-D223 | A real package that isn't declared (yaml comes from PyYAML), or an import Skylos couldn't confirm | Your dependency files |
| SKY-D224 | A function that the installed version of a library doesn't have | The library installed where Skylos runs; by default it inspects requests, pandas, boto3, openai and jwt |
| SKY-L012 | A call to a member that a local module doesn't define | The local module, resolved statically |
yaml is the usual import-name trap: the import is yaml, the distribution is PyYAML. The same goes for cv2 (opencv-python) and sklearn (scikit-learn).
SKY-D222 or SKY-D223 depends on your pins
Skylos only calls a Python import "hallucinated" when it can rule out that one of your declared dependencies provides a module with that name. It does that by reading the declared versions' file lists from PyPI. On the same fixture:
dependencies in pyproject.toml | cryptoutils reported as |
|---|---|
["requests==2.32.3"] | SKY-D222 "Hallucinated dependency 'cryptoutils'. Package does not exist on PyPI." |
["requests>=2.31"] or ["requests"] | SKY-D223 "Unverified import 'cryptoutils'. No PyPI project has this name, and the declared dependency 'requests' could not be checked for this import path, so it is not confirmed as an undeclared or hallucinated dependency." |
Either way the made-up name is reported, and --select SKY-D222,SKY-D223 --strict exited 1 in all three cases. Select both rules; don't gate on SKY-D222 alone.
Block the install before it happens
If an AI agent runs the install itself, checking the code afterwards is late: an install can run the package's code (a source build's setup.py, or a .pth file on every Python start) before anything imports it. skylos agent install-hooks adds a hook that runs before pip, uv, poetry, npm, pnpm, yarn, bun and go installs in Claude Code, Codex and Cursor. Fed pip install cryptoutils flask-secured, it answered:
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Skylos blocked this install: 2 packages look hallucinated or typosquatted.\n- PyPI package 'cryptoutils' does not exist on PyPI (likely hallucinated; an attacker can register it).\n- PyPI package 'flask-secured' does not exist on PyPI (likely hallucinated; an attacker can register it).\nCheck 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."}}
It also flags one-edit look-alikes of popular names (npm install expresss was denied as "a possible typosquat ... 'express'"). Commands that use a private index or registry skip the public lookup. Details and the Codex and Cursor variants: Claude Code hooks examples for security.
Fail the pull request
name: hallucinated-packages
on: pull_request
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- run: python -I -m pip install "skylos==5.1.1"
- name: Fail on made-up or undeclared packages
run: skylos . --select SKY-D222,SKY-D223 --strict
Two things we checked that are easy to get wrong:
--gatealone doesn't fail on these. On the fixture above,skylos . --ai-defects --gateexited 0 with three CRITICAL SKY-D222 findings, because the default gate allows up to 10 AI-defect findings. Withmax_ai_defects = 0under[tool.skylos.gate]it exited 1.- SKY-D224 needs your dependencies installed. Add
pip install -e .(or your lockfile install) before the scan if you want installed APIs checked.
For a whole-repository scan with security, secrets and AI defects, run skylos . -a locally. --ai-defects is part of -a.
What this does not catch
- A hallucinated name someone has already registered. PyPI then says it exists and SKY-D222 stops firing. SKY-D223 still fires if the package isn't in your dependency files, but once it is declared, nothing in this check flags it. Review the publisher, age and source of every new dependency.
- Wrong submodule paths in real packages, such as the pre-0.1
from langchain.chat_models import ChatAnthropic. The package exists; the path may not, depending on the version. Run the code or an import check against your lockfile. - JavaScript imports without a manifest entry. In our test,
require("express-secure-session-helperx")in a.jsfile was not reported; the same name inpackage.jsonwas. - APIs of libraries outside the inspected set for SKY-D224, and dynamically built clients.
- Installs from manifests or URLs in the hook (
pip install -r,npm ci,git+https://...). The code scan covers manifests.
Other tools
Several small open-source tools target the same problem, including dep-hallucinator and mirago on PyPI and pkghall on GitHub, and Endor Labs' explainer covers manual checks. They differ in which ecosystems they check and whether they also score package age and downloads, which Skylos's code scan does not. Skylos's angle is that the same CLI also checks local-module members, installed APIs and the agent's install commands.
Related
- Slopsquatting in Python: what package hallucination research means
- Claude Code hooks examples for security
- Verify AI-generated code: check the agent didn't delete or skip tests
- Is AI-generated Python code insecure? What the 2026 research shows
- Vibe coding security checklist for Python apps
- SonarQube alternative for AI-generated code: Skylos vs SonarQube
Running this across a team? These checks are free in the CLI. Skylos Cloud keeps the results in one place: the Free plan adds a pass/fail check on pull requests and scan history for one project, and the Workspace plan adds up to 10 projects, approvals for findings you decide not to fix, pull request comments, and audit exports.