depverify
Checks whether the packages an LLM told you to pip install actually exist.
Advisory only. Zero LLM calls. Never installs, deletes, halts, or quarantines anything.
The problem, in 30 seconds
An LLM suggests this fix for you:
To build a quick CLI progress tracker with retry logic, install these:
pip install requests
pip install retry-backoff-pro
pip install tqdm
Then in your script:
import requests
import tqdm
from retry_backoff_pro import RetryPolicy
policy = RetryPolicy(max_attempts=3)
requests and tqdm are real. retry-backoff-pro reads like a real,
small retry-helper library. It isn't -- it does not exist on PyPI. If an
attacker registers that exact name with malware before you run the
install (a real technique known as slopsquatting), pip install
will happily fetch it and your script will happily import it.
$ depverify check llm_answer.txt
✅ requests [pip_install] EXISTS
✅ tqdm [pip_install] EXISTS
➖ retry_backoff_pro [import] LOCAL_OR_UNKNOWN
❌ retry-backoff-pro [pip_install] NOT_FOUND
checked=4 not_found=1 suspicious=0 cant_verify=0
That's the whole tool. Deterministic report, one command.
Why
LLM coding models hallucinate package names. USENIX Security 2025 (Spracklen et al.) measured ~19.7% of recommended packages as hallucinated across 16 models (~5% commercial, ~21% open-source); a May 2026 replication found frontier models compressed to ~4.6-6.1%, but quantized local models remain far worse. Attackers register hallucinated names on PyPI with malware ("slopsquatting").
Every verdict is deterministic. Extraction is regex/AST, existence is a PyPI lookup, reputation is arithmetic over metadata. No LLM judging calls anywhere in this codebase -- the tool that checks for hallucinations doesn't itself hallucinate.
| depverify | Content-scanning tools (Socket, Snyk, etc.) | |
|---|---|---|
| Checks name exists | ✅ | usually assumed, not the focus |
| Checks package content/behavior | ❌ (by design -- see What this does NOT do) | ✅ |
| Blocks installs / fails CI | ❌ (report only) | often yes |
| Model calls involved | 0 | varies by product |
| npm / yarn | ❌ (Python/PyPI only) | often yes |
Install
pip install depverify # core
pip install depverify[flask] # + flask itself, for the HTTP integration below
depverify[flask] installs the flask dependency only. The HTTP
integration itself (integrations/cortexfeed/flask_blueprint.py) is
source-only, deliberately not part of the installed depverify package
-- see Flask HTTP integration below.
Dev: pip install -e ".[flask,dev]" (pytest, pandas, flask).
Usage
depverify check path/to/answer.txt
cat answer.txt | depverify check -
depverify check answer.txt --json
depverify check answer.txt --symbols
depverify check answer.txt --ignore my-internal-lib --ignore 'acme-*'
A successful check's exit code is always 0, regardless of what it finds
(NOT_FOUND, suspicious, etc.) -- this is an advisory tool, not a
linter that fails your build over the contents of a report. That
guarantee covers verdicts, not invocation: a CLI usage error (bad
arguments, a missing/unreadable input file, non-UTF-8 input) is reported
as a clean one-line message on stderr and exits non-zero, the same way
depverify check with a missing required argument already does -- it is
not itself a verdict, so it is not covered by the "always 0" guarantee.
If depverify finds nothing at all, it says so and explains why -- it
only reads literal pip install <name> lines and import x / from x import y statements from raw text, not a requirements.txt-style file of
bare name==1.2.3 entries, and not pyproject.toml/Pipfile/setup.py
dependency tables. Point it at the actual text an LLM gave you, not your
dependency manifest.
--symbols additionally checks statically-resolvable attribute chains
(e.g. pandas.DataFrame.flatten) against packages already installed in
the local environment. Security note: this is the one depverify
feature that imports locally installed code, which can execute that
code's import-time side effects -- see
Security note: --symbols and local code execution
before enabling it. Off by default; normal depverify check never
imports or executes anything locally installed.
Internal / private packages
If you run a private package index (Artifactory, Nexus, an internal PyPI
mirror), your own package names will otherwise come back NOT_FOUND or
LOCAL_OR_UNKNOWN -- depverify only ever checks public PyPI, and has no
way to know a name is legitimate on your infrastructure. Recognize those
names so they're reported as INTERNAL instead, never queried
against PyPI at all:
depverify check answer.txt --ignore my-internal-lib
depverify check answer.txt --ignore 'foo,bar,acme-*' # repeatable and/or comma-separated
depverify check answer.txt --ignore-file .depverify-ignore
Or via a .depverify.toml in your project root (auto-discovered; no flag
needed), or an explicit path with --config:
[internal]
names = ["my-internal-lib", "acme-*"]
All three sources are mergeable. Matching is name-only (never a network
call), case-insensitive, PEP-503-normalized (so My_Internal.Lib,
my-internal-lib, and MY-INTERNAL-LIB are all the same pattern), and
supports shell-style glob wildcards via fnmatch (acme-*, not regex).
INTERNAL applies to a matched name from either a pip install line
or a bare import -- unlike LOCAL_OR_UNKNOWN, it's a confirmed
verdict, not a "might be yours" guess. This feature is entirely opt-in:
with no --ignore/--ignore-file/--config/.depverify.toml in play,
behavior, verdicts, and Report.summary()'s shape are unchanged from
before this feature existed (summary()'s internal key is present
only when at least one INTERNAL verdict actually occurs).
Library
from depverify import verify_text
report = verify_text(llm_answer_text)
print(report.to_json())
Verdict model
Per detected dependency:
| Verdict | Meaning |
|---|---|
EXISTS |
Confirmed on PyPI. Carries a risk object (see below). |
NOT_FOUND |
Confirmed not on PyPI. Only assigned to pip install lines. |
STDLIB_SKIPPED |
Part of the Python standard library -- not a PyPI lookup at all. |
LOCAL_OR_UNKNOWN |
Unresolved import. May be a local module, a relative import, or an unconfigured private package (see INTERNAL below to stop guessing and configure it). |
INTERNAL |
Matched a configured internal/private-package name or pattern (--ignore/--ignore-file/.depverify.toml, see Internal / private packages) -- not a PyPI lookup at all, applies to both pip install and import sources. |
CANT_VERIFY |
Lookup itself failed (network, rate limit, etc.) -- not a verdict on the package. |
EXISTS packages additionally carry a risk object:
level ∈ ok | suspicious, with
reasons ⊆ {young_package, low_downloads, near_name:<popular-package>, version_missing}.
Existence and reputation are orthogonal. A package that EXISTS can
still be suspicious. depverify never folds "sketchy" into "missing" --
those are different questions with different implications.
The core asymmetry
A name that 404s on PyPI is interpreted differently depending on how it was detected:
| Detected via | Verdict | Why |
|---|---|---|
pip install <name> |
NOT_FOUND |
The LLM told you to install this; it doesn't exist; that's the signal slopsquatting exploits. |
import <name> |
LOCAL_OR_UNKNOWN |
Might be your own module, a relative import target, or a name that isn't on PyPI for a legitimate reason. Flagging every unresolvable import as "not found" would bury real warnings under false positives from ordinary project code (import myapp.models, etc.). |
Report shape (click to expand)
{
"packages": [
{"name_raw": "cv2", "resolved": "opencv-python", "source": "import",
"verdict": "EXISTS", "risk": {"level": "ok", "reasons": []}},
{"name_raw": "pdfreader-pro", "resolved": "pdfreader-pro", "source": "pip_install",
"verdict": "NOT_FOUND", "risk": null}
],
"summary": {"checked": 2, "not_found": 1, "suspicious": 0, "cant_verify": 0}
}
summary.cant_verify counts only package-level CANT_VERIFY verdicts. It
does not include symbol-level CANT_VERIFY results (e.g. a --symbols
chain that couldn't be checked because the package isn't installed
locally) -- those live under each package's own symbols list (see
--symbols below) and are a separate count from the top-level summary.
This is deliberate: summary() predates the symbol-checking feature and
was kept unchanged so existing consumers of Report.summary()/to_dict()
see byte-identical output when check_symbols is left at its default.
Reputation signals (EXISTS packages only)
| Signal | How it's computed |
|---|---|
| Age | Days since the earliest PyPI release. |
| Downloads | Last-30-day count from pypistats.org. If that API is unreachable or rate-limited, depverify silently omits download-based reasons -- a missing download count is never treated as CANT_VERIFY. |
| Near-name | difflib.SequenceMatcher.ratio() >= 0.88 (tunable, see reputation.py) against a vendored list of popular package names (depverify/top_packages.json), flagged as near_name:<popular-package> when the candidate itself isn't already a popular package. |
Default suspicion rule (a tunable default, not ground truth):
suspicious if (age < 60 days AND downloads < 1000/month) OR any near_name hit.
Tune the constants in depverify/reputation.py for your risk tolerance.
What this does NOT do
- No package-content scanning. depverify checks whether a name exists and looks reputable by metadata -- it does not download, sandbox, or static-analyze package code. For that, see dedicated tools like Socket or Snyk.
- No blocking. Nothing here gates a pip install, a CI job, or an LLM response. It's a report.
- No npm/yarn. Python/PyPI only, by design.
- No LLM-based judging. Every verdict is deterministic: extraction is regex/AST, existence is a PyPI lookup, reputation is arithmetic over metadata. No model calls anywhere in this codebase.
Security note: --symbols and local code execution (click to expand)
Security note: --symbols and local code execution
Normal depverify check (no --symbols) never imports or executes any
locally installed package code. Existence checks are PyPI metadata
lookups over HTTP; reputation checks are pypistats.org lookups and
arithmetic. Nothing in that path touches your local Python environment's
installed packages, and nothing ever executes the LLM-generated code
being scanned.
--symbols is different, and deliberately scoped narrowly because of it:
- To check whether an attribute chain like
pandas.DataFrame.flattenreally exists,--symbolsusesimportlib.import_module()to import the already-installed package andgetattr()to walk the chain. Importing a Python module runs that module's top-level code -- this is ordinary Python behavior, not something depverify adds, but it means--symbolsis the one code path in this tool that executes local code as a side effect of scanning. - This is not the same as executing the LLM's code.
--symbolsnever runs the snippet being checked; it only imports packages by name usingimportlib, and only ever callsgetattr/hasattron the resulting module or class objects -- never instantiates a class, never calls a function or method. (Confirmed intests/test_symbols.py: walking a chain that reaches a property or method never triggers the property getter, the method body, or the class's__init__.) --symbolsnever installs anything. If a package exists on PyPI but is not already installed locally, the result isCANT_VERIFY, not an install-then-import. depverify's "never installs, deletes, halts, or quarantines anything" guarantee holds for--symbolstoo.- The residual risk: if a malicious or already-compromised package
happens to be installed in the same environment running depverify,
--symbolswill trigger that package's import-time code the same way a plainimport thatpackageanywhere else in that environment would. depverify does not sandbox, isolate, or vet locally installed packages before importing them for a symbol check. - Consequently: only enable
--symbolsin an environment where the installed package set is already trusted -- the same trust you'd already extend to runningpython -c "import <installed package>"in that environment. Do not run--symbolsas a way to safely inspect an environment whose installed packages you don't already trust, and do not present or rely on--symbolsas a sandbox or security boundary around untrusted local installs -- it is not one.
Limitations (click to expand)
- Dynamic imports are not detected.
importlib.import_module("name")string literals are invisible to the AST/regex extraction inextract.py. Only literalimport x/from x import ystatements andpip installlines are found. - Only pip-style dependency declarations are parsed.
setup.py,pyproject.toml([project.dependencies]), Conda environment files, and Poetry'spyproject.tomldependency tables are not parsed. - The import-name -> distribution-name mapping table is incomplete by
nature.
depverify/mapping_table.pyis hand-curated (71 entries as of this writing) and will always miss some real-world aliases. Unmapped import names fall back to querying PyPI with the import name as-is, which works for the common case (import name == distribution name) but not for every alias. - Verification is point-in-time. A name that 404s right now can be registered on PyPI minutes later -- including by an attacker watching for exactly this kind of hallucinated name (slopsquatting). Nothing here caches a "safe" verdict indefinitely; the cache TTLs (24h for EXISTS, 6h for NOT_FOUND) reflect that names that don't exist yet are the more time-sensitive case.
- EXISTS ≠ safe: compromised legitimate packages are invisible to this tool. A package that exists, is old, and has millions of downloads can still ship malware in a compromised release. depverify's existence and reputation checks say nothing about supply-chain compromise of an otherwise-legitimate package.
depverify/top_packages.jsonwas hand-vendored, not fetched live, because the environment this project was built in could not reachraw.githubusercontent.com,api.github.com, or any CDN mirror (onlypypi.org/files.pythonhosted.org/ baregithub.comwere reachable). It's a ~450-name curated list of well-known packages written from training knowledge, not the real top-5000hugovk/top-pypi-packagesdataset. Runscripts/fetch_top_packages.pyfrom a network with GitHub raw-content access to regenerate the real list before relying on near-name detection for anything beyond obvious cases.- Attribute-chain verification (
--symbols) only covers simple, statically-resolvable chains rooted directly in an imported name (e.g.pandas.DataFrame.flatten). It does not do instance attribute inference, signature/arity checking, type inference,.pyistub parsing, or follow chains past a function call (requests.get(url).jsonis not checked pastrequests.get) or past a reassigned import. Seedepverify/symbols.py's module docstring for the full scope, and Security note:--symbolsand local code execution before enabling it -- unlike the rest of depverify, it imports locally installed code.
Flask HTTP integration
integrations/cortexfeed/flask_blueprint.py is a small, source-only Flask
Blueprint exposing verify_text over HTTP (POST /verify). It is
not part of the installed depverify package -- pip install depverify[flask] only pulls in the flask dependency it needs, not this
file itself. That's intentional: this blueprint is meant to be copied
into your own Flask app (it imports only from depverify, nothing from
cortexfeed, so it has no hidden dependency on the project it's named
after), not imported from a pip-installed depverify. To use it, copy
integrations/cortexfeed/flask_blueprint.py from a source checkout into
your own project and register the blueprint it defines:
from flask_blueprint import depverify_bp # after copying the file in
app.register_blueprint(depverify_bp)
See the file's own module docstring for the request/response shape.
Project layout
depverify/
├── depverify/ # the library + CLI -- this is what `pip install depverify` ships
├── integrations/cortexfeed/ # source-only Flask blueprint; copy into your app, not pip-installed
├── tests/ # unit tests (mocked, zero network) + eval/ (live network)
└── scripts/ # fetch_top_packages.py: regenerate the vendored top-package list
Development
pip install -e ".[flask,dev]"
pytest tests/ --ignore=tests/eval # unit tests, zero network
python tests/eval/run_eval.py # eval, live network against PyPI/pypistats
No blocking. No LLM calls. Just an honest answer to "does this package exist?"
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file depverify-0.2.0.tar.gz.
File metadata
- Download URL: depverify-0.2.0.tar.gz
- Upload date:
- Size: 63.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ba73e789b276e0e9482ee4235fe7f97114d7c73b96b1afe351ad0dec00c4d0d4
|
|
| MD5 |
9ae9dacfe9c235dcf8ce7ce47aec693a
|
|
| BLAKE2b-256 |
4a2fe7f55afdd0f01cd8f9c7e876add91aebf31463485ef9f0d97487080d336d
|
Provenance
The following attestation bundles were made for depverify-0.2.0.tar.gz:
Publisher:
publish.yml on AeroScissors/Depverify
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
depverify-0.2.0.tar.gz -
Subject digest:
ba73e789b276e0e9482ee4235fe7f97114d7c73b96b1afe351ad0dec00c4d0d4 - Sigstore transparency entry: 2438740119
- Sigstore integration time:
-
Permalink:
AeroScissors/Depverify@04f26fbdbf2a09da4f843daf34f88727c595f856 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/AeroScissors
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@04f26fbdbf2a09da4f843daf34f88727c595f856 -
Trigger Event:
release
-
Statement type:
File details
Details for the file depverify-0.2.0-py3-none-any.whl.
File metadata
- Download URL: depverify-0.2.0-py3-none-any.whl
- Upload date:
- Size: 47.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
10a6de3d0f1ce557d5401fdc4453963c06c3db20f2cb3742a8aed63ca6ce3575
|
|
| MD5 |
91d5f54b457f6b66dff64480d9f3cd15
|
|
| BLAKE2b-256 |
59886f618fd5aeff09008866da50358389dc104d6c02b166275d84883aee1eb4
|
Provenance
The following attestation bundles were made for depverify-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on AeroScissors/Depverify
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
depverify-0.2.0-py3-none-any.whl -
Subject digest:
10a6de3d0f1ce557d5401fdc4453963c06c3db20f2cb3742a8aed63ca6ce3575 - Sigstore transparency entry: 2438740137
- Sigstore integration time:
-
Permalink:
AeroScissors/Depverify@04f26fbdbf2a09da4f843daf34f88727c595f856 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/AeroScissors
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@04f26fbdbf2a09da4f843daf34f88727c595f856 -
Trigger Event:
release
-
Statement type: