How to catch hallucinated function and file names in AI-generated documentation
Check every file path and code name the model writes against the repository itself, in code, before anyone reads the documentation.
AI-written documentation can name functions and files that were never written, and those names look exactly like the real ones beside them. The good news is that this kind of mistake can be caught by a program, without asking another model.
What is an AI hallucination?
An AI hallucination is output from a language model that sounds fluent and confident but is not supported by its input or by the facts: a function that does not exist, a file nobody wrote, a test result nobody ran. In documentation it is especially costly. A reader who trusts the page will search for the name, fail to find it, and then wonder what else on the page is wrong.
Why does AI hallucinate?
A language model writes the words most likely to come next, given what it has read. It does not look anything up while it writes. Where the material is thin, the most likely words are often a plausible name rather than a real one. A signing library could well have a Signer.verify_token method; whether a given library has one is a fact the model cannot check from inside the sentence.
Prompts help but do not cure it. You can tell a model to mention only files and functions present in the material, never to invent test counts or versions, and to say when it is unsure. Some invented names still get through, so you need a check after the writing.
The hallucination you can check
Most hallucinations are hard to catch. A wrong explanation of what a function does reads exactly like a right one. But one kind is mechanically checkable. When documentation says "call Signer.rotate_keys()" or "configuration lives in config/settings.py", it makes a claim about names that either exist in the repository or do not.
So check that kind in code. A program that looks names up is free, instant and reproducible, and the model being checked cannot argue with it. Save human review, or a second model, for the claims a program cannot settle.
How to check names against the code
The method has three parts, and none of them needs a model call:
- Build a list of what exists. Every file and folder at the commit you documented, plus a symbol index: the classes, functions and methods defined in the code. For Python, the standard
astmodule gives you exact definitions. Other languages can be indexed with per-language patterns, which are less exact. - Pull out the claims. Take the code-like tokens the documentation writes in backticks: paths, calls, dotted names,
snake_caseandCamelCasenames. Plain prose is left alone. - Look each one up. A path is found if the file or folder exists. A dotted name such as
module.functionis found only if the module exists and the function is in the index.
A first version fits in a few lines:
import ast, pathlib, re
symbols = set()
for f in pathlib.Path(repo).rglob("*.py"):
for node in ast.walk(ast.parse(f.read_text())):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
symbols.add(node.name)
claims = re.findall(r"`([^`\n]+)`", doc)
Everything after that is refinement, and most of it comes from real misses.
Rules that came from real misses
Each rule below was written after a checker let something through or flagged something real:
| What happened | Rule now |
|---|---|
Signer.rotate_keys() passed because Signer exists |
the last part of a dotted name must exist |
URLSafeSerializer.encrypt passed because "encrypt" appears in prose |
the last part must be used as code |
path/to/tinycalc in a guide was flagged |
placeholders the reader replaces are skipped |
exit() was flagged |
language built-ins always exist |
threading.Lock was read as a .lock file |
file extensions are case-sensitive |
Built-ins need care in the other direction too: str.join exists, but str.frobnicate should be flagged.
Resolve citations in code, never trust line numbers
If your documentation links claims to code, do not ask the model for line numbers. Ask it to cite a file and a symbol, such as signer.py::Signer.sign, and resolve that to a line range yourself from the index.
What happens when resolution fails matters more. A citation that cannot be resolved should stay plain text and be listed for the reader. It should never become a precise-looking link to the wrong place.
Six names, checked one by one
Here is the method applied to documentation for itsdangerous, a small Python signing library, using names from Nehemvix's evals:
src/itsdangerous/token_cache.py: no such file. Flagged.Signer.rotate_keys():Signeris real,rotate_keysis not. Flagged by the last-part rule.json.dumps_safe:jsonis a real standard-library module with no such member. Flagged.signer.py::rotate_keysas a citation: it does not resolve, so it stays plain text and is listed.src/itsdangerous/signer.py::Signer.sign: resolves to the lines wheredef sign(is, and becomes a link.- "README.md" for a repository whose README has no extension: a wrong file name our hand-checked guides record as a known mistake.
One example came from a live run. On 16 October 2026 a guide for the JavaScript utility lukeed/clsx invented an example variable, isActive. The check caught it, and the repair step described below replaced it with the real usage pattern.
How Nehemvix uses the result
Nehemvix runs this check on every guide it writes, between writing and publishing. It indexes Python exactly and a dozen other languages, such as JavaScript, Go and Rust, with patterns. The result then does two jobs.
One repair, kept only if it is better
When a section contains names the checker cannot find, the model gets one repair call that lists them: keep a name only if it is real and cite where it is defined, otherwise remove it and never invent a replacement. The repair is kept only if it has fewer unverified names than the original. Otherwise the original stays and the names are listed for the reader under "Names we couldn't find in the code".
The grounding score
Each guide gets a grounding score from 0 to 100. It weighs whether the required sections are present, how many names and citations failed to check, how much of the important code the guide covers, whether the diagram is well-formed and how readable the prose is. It is labelled "structure and grounding, not factual correctness" wherever it appears, because that is what it measures.
What this cannot catch
Be explicit about the limits, and pin them with tests so the list cannot quietly become untrue:
- A false claim made only of real names. "
TimestampSignerencrypts every payload with AES" uses a real class, and no name check will notice. - Names outside backticks are not checked.
- Pattern-matched languages: an invented name that happens to appear as code elsewhere in the repository passes.
- Third-party imports cannot be verified offline and pass.
If the documentation you need to check wasn't written by Nehemvix, we describe the same checks as a method for any AI-written docs, with no LLM judge.
How well the check works should be measured, not assumed. In a blind round written by a reviewer who never saw Nehemvix's checker, it caught 94.3% of planted names, and in a harder second round 83.3%, without wrongly flagging a real name. The method is on how every guide is checked.
Checklist
- Index the files and symbols at the exact commit you documented.
- Check every backticked name, and require the last part of a dotted name to exist.
- Skip placeholders and language built-ins, deliberately.
- Resolve citations yourself, and list the ones that fail.
- Give the model one repair, and keep it only if it is better.
- Tell the reader which names could not be found.
- Write down what the check cannot catch.
Related reading: how these numbers are kept from slipping is in building an eval gate for an LLM app, and what a reader sees in a finished guide is in reading a guide.
Learning this to build your own agent? AELSHA™ can help you design it.