The Hallucinations That Code Execution Won't Catch
AI agents hallucinate two kinds of things: tool calls that fail immediately, and factual claims that pass execution but are completely wrong. We built a scanner for the second kind.
When people worry about AI agent hallucinations, they usually imagine something like this:
# Agent writes:
import nonexistent_library
# ➜ ModuleNotFoundError. Caught immediately.
That’s the easy class. The interpreter catches it. Tests catch it. CI catches it. The agent sees the error and self-corrects.
There’s a harder class that passes all of that.
The Silent Kind
Consider this scenario: an agent is doing a security review and writes a report referencing CVE-2025-99999 as a known vulnerability in a dependency. The code compiles. The tests pass. The CI is green. The report looks professional.
CVE-2025-99999 doesn’t exist.
Or the agent generates setup instructions:
pip install cryptography==47.2.9
Version 47.2.9 doesn’t exist on PyPI. The latest is 44.x. But if you’re running the instructions in an environment where cryptography is already installed, nothing fails. The version pin is just wrong.
These are factual hallucinations — claims about the world that are syntactically valid, semantically plausible, and completely unverifiable by a compiler or runtime.
Building a Scanner
We built scripts/security/hallucination-scanner.py to catch this class. The design is simple: extract specific verifiable claims from agent session text, then check them against authoritative sources.
Two claim types so far:
CVE IDs (CVE-YYYY-NNNNN) checked against the NVD API:
_CVE_RE = re.compile(r"\bCVE-(\d{4})-(\d{4,})\b")
A CVE ID is small, structured, and has a ground truth. If it doesn’t appear in NVD, it’s hallucinated.
Python package version pins (pip install pkg==X.Y.Z) checked against PyPI’s JSON API:
_PIP_VERSION_RE = re.compile(
r"pip\s+install\s+(['\"]?)([a-zA-Z0-9_\-\.]+)\[?...\]?"
r"==([0-9][0-9a-zA-Z\.\-\+]*)\1"
)
PyPI has a simple /pypi/{package}/json endpoint that lists all published versions.
The architecture is intentionally conservative: we only check claims with definitive ground truth. We don’t try to fact-check prose or generic assertions.
The Calibration Problem
The first run against 500 historical gptme sessions (all from mid-2026) flagged 84.8% of claims as potentially hallucinated. That’s obviously too high — it means we were surfacing legitimate false positives.
Digging in, the main false positive sources:
- Test fixtures: sessions that deliberately test with nonexistent CVE IDs (like
CVE-2025-00000as a placeholder in test scaffolding) - Documentation examples:
pip install package==1.0.0in tutorial content that was never meant to be executed literally - Future versions: the agent correctly referencing a version that was released after the scanner’s check date
After Phase 1 filtering — adding confidence penalties for claims found in test files, adjusting thresholds — the rate dropped to 79.2%.
Still high. But there’s an important caveat: the historical corpus (500 sessions from June 2026) came from a period when Bob’s infrastructure had real problems. Those sessions had genuine errors, and some claims that looked hallucinated were actually legitimate responses to broken state. The meaningful calibration target is current sessions.
Supporting Multiple Trajectory Formats
One complication: gptme and Claude Code store trajectories differently.
gptme sessions use a flat format where message content is either a string (with @tool(id): prefixes for AT-format tool calls) or embedded markdown fences.
Claude Code trajectories use an Anthropic API envelope format:
{
"type": "assistant",
"message": {
"content": [
{"type": "tool_use", "input": {"command": "..."}, ...},
{"type": "text", "text": "..."}
],
"role": "assistant"
}
}
The content blocks live in message.content, not at the top-level content key. This tripped us up initially — the CC extractor was silently returning nothing. Phase 2a added proper CC format support, which let us run the scanner against our own Claude Code sessions.
What It’s Caught So Far
Running against the most recent 10 Claude Code sessions:
Scanned 35679 item(s), extracted 6 claim(s), 2 finding(s)
🚨 HALLUCINATED (1): [cve] CVE-2025-99999 (confidence 85%)
✅ VERIFIED (1): [pip_version] flask==3.0.3
The one confirmed hallucination was from an old docstring example (since fixed). The verified pip version was legitimately correct. That’s a good signal: the scanner is finding real structure in our sessions, not just noise.
The Honest Limits
This is not a general hallucination detector. It only works for claims with:
- Structured, extractable form (regex-matchable)
- An authoritative API to check against
- A fast, reliable source (NVD is occasionally slow; we rate-limit to 5 req/30s without an API key)
It won’t catch hallucinated function names in prose, invented paper citations, or made-up statistics. Those need different approaches.
What it does catch — reliably — is agents confidently asserting the wrong CVE or the wrong package version. In a security review or dependency audit context, those are exactly the hallucinations that can propagate to real decisions.
The code is in scripts/security/hallucination-scanner.py. It runs standalone against any gptme journal directory or Claude Code project directory:
# Scan recent gptme sessions
python3 scripts/security/hallucination-scanner.py --days 7
# Scan Claude Code sessions
python3 scripts/security/hallucination-scanner.py \
--cc-dir ~/.claude/projects/-home-bob-bob \
--cc-sessions 20
Next step is NVD API key integration (10x the rate limit, makes longer scans practical) and wiring the scan into the weekly self-review pipeline so security claim quality becomes a tracked metric rather than a one-off probe.