Fail-closed existence verification for model-emitted identifiers — catches hallucinated (vaporware) packages, paths, and symbols before an agent acts on them.
Project description
vaporcheck
Fail-closed existence verification for model-emitted identifiers. Catch hallucinated and slop-squatted package names, dead file paths, and nonexistent API/tool names before an agent acts on them — because vaporware you pip install is somebody else's payload.
Why
LLM coding agents confidently emit identifiers that don't exist: a USENIX Security 2025 study found 19.7% of LLM-recommended packages were hallucinated — an open door for slopsquatting (an attacker registers the hallucinated name). The same failure shows up as dead file paths, deprecated/removed APIs, and calls to tools that were never registered.
The root cause — model overconfidence — is not fixable by a wrapper. So vaporcheck doesn't try. It replaces the unanswerable question "is the model confident?" with a deterministic one:
"Does the referent actually exist in its authoritative source?"
That's a binary lookup (a registry, the filesystem, a symbol table) — testable, cheap, and hard to argue with.
How it works
A small resolver core answers exists / not-found / deprecated / cannot-verify for one identifier, shipped through two delivery vehicles:
| Vehicle | What it is | Guarantee |
|---|---|---|
MCP server (verify_identifier tool) |
Any MCP client can ask "does this exist?" | Agent-callable, advisory |
| PreToolUse hook | A Claude Code hook that denies the tool call | Fail-closed — the agent can't proceed |
Both reuse the same resolvers. Standalone, the MCP is advisory (the model may ignore it); paired with the hook (or any gateway that can block), it becomes a hard gate.
The honest boundary
A hook sees tool calls, not generated text. So today this gates the two classes that surface at the tool boundary:
- package installs (
pip install,npm install, …) — verified against PyPI / npm - file paths (
Editof a missing file) — verified against the filesystem
Tool-names and API-symbols buried in generated code are verify-on-write / verify-on-run (gate the Edit or test that introduces them), not mid-sentence interception. This is a deliberate, documented limit — see docs/SPIKE-FINDINGS.md.
Install
Requires Python 3.10+. Zero runtime dependencies (stdlib only — fitting for an anti-supply-chain-risk tool).
Option A — pip (adds the vaporcheck-mcp console command)
pip install vaporcheck
Option B — clone (if you want the hook/MCP config to point at source files)
git clone https://github.com/cdmx-in/vaporcheck.git
cd vaporcheck
python vaporcheck/test_parse.py # offline smoke test — should print 10/10 PASS
Configure
As an MCP server (any MCP client)
// .mcp.json (project scope) — adjust the path to where you cloned
{
"mcpServers": {
"vaporcheck": { "command": "python", "args": ["/path/to/vaporcheck/vaporcheck/server.py"] }
}
}
(On Windows, "command": "py", "args": ["-3", ...] also works. If you installed via pip, use "command": "vaporcheck-mcp" with no args.)
Then call the verify_identifier tool:
{ "kind": "pypi", "value": "reqeusts" }
// -> { "status": "not-found", ... } (a slopsquat — blocked)
As a fail-closed Claude Code hook
// .claude/settings.json (or settings.local.json)
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash",
"hooks": [{ "type": "command", "command": "python /path/to/vaporcheck/vaporcheck/hook.py", "timeout": 15 }] }
]
}
}
Restart Claude Code to load the hook. Now pip install <hallucinated-package> is denied before it runs, with a reason the model can self-correct from. On network failure the hook degrades to ask instead of falsely blocking.
Status
MVP — working, tested, dogfooded. Resolvers: PyPI, npm, filesystem paths. Shared SQLite cache (warm lookups ~1 ms) with status-aware TTLs and a token-bucket throttle.
Roadmap
- More package ecosystems: crates.io, Go, RubyGems, Maven
- Tool-name resolver (against the live tool registry) — greenfield, no incumbent
- API-symbol resolver (via LSP / compiler) — greenfield
- Citations resolver (last; the crowded class — breadth, not headline)
- Namespace the modules under a package; publish to PyPI
Tests
38 checks across 4 suites — all hitting real registries / filesystem, no mocks:
python vaporcheck/run_spike.py # hook behavior (10)
python vaporcheck/test_parse.py # shell-aware install parser (10)
python vaporcheck/test_mcp.py # MCP client handshake (9)
python vaporcheck/test_cache.py # cache latency / TTL / errors (9)
Provenance
This repo is the product of a structured research → validation → build effort. The full record — pain-point research, web-verified go/no-go, and the live-dogfooding spike that found and fixed two real parser bugs — is in docs/.
License
Project details
Release history Release notifications | RSS feed
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 vaporcheck-0.1.0.tar.gz.
File metadata
- Download URL: vaporcheck-0.1.0.tar.gz
- Upload date:
- Size: 21.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7761d94a7e3021fe69e4e60c466c43e5c26e457fb297985208fac2fdb037fc02
|
|
| MD5 |
c0095b1bcc8aae9c67416fb9c56117dd
|
|
| BLAKE2b-256 |
d920744007c8c2f0990c442bd5aa5604a67eae0beb423d526812f8456c6e2966
|
File details
Details for the file vaporcheck-0.1.0-py3-none-any.whl.
File metadata
- Download URL: vaporcheck-0.1.0-py3-none-any.whl
- Upload date:
- Size: 22.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
15fbf68d6337d9586b8927195d61d8d26ea6ca2118b8eb7e86c71ca77b576bab
|
|
| MD5 |
67fffc6475e104de770da846f7adec1a
|
|
| BLAKE2b-256 |
3db88ec05f0d2ff87be0e09ec40bb9c53342e79f443bcf7ce8ce0fd2a3bfa87b
|