An agent tells you the fix lives in src/services/billing/retry.ts. It sounds right. The path looks like your project, the naming matches your conventions, the explanation is fluent. So you act on it, and only later find out the file does not exist. There is a src/services/billing/ directory, but the retry logic is in queue.ts, and the agent invented the rest.
This is the most common shape of LLM hallucination in code, and it is also the cheapest to catch. You do not need another model, a vector database, or a clever prompt. You need one boring habit: before you trust a reference, check that it exists.
Why file paths are the hallucination worth checking first
A model that writes about your codebase is constantly making small factual claims. This file exists. This function is exported. This module imports that one. This config key is called maxRetries. Each of those is a reference, and a reference is either real or it is not. There is no interpretation involved.
That is what makes them special. Most of what a model produces is a judgment you can argue about. A file path is not a judgment. It is a lookup with a yes or no answer, which means a five-line function can verify it while the model is still talking. If you only ever add one guardrail to an AI coding workflow, make it this one, because it has the best ratio of damage prevented to effort spent.
Hallucinated references are also the errors that propagate the worst. A wrong opinion tends to get caught by a human reading the output. A wrong file path sails through, because it looks exactly like a right one, and the next step in your pipeline builds on it. By the time anything breaks, the false reference is three steps upstream and hard to trace. This is the same problem described in structured output guarantees the shape, not the truth: the output is well formed, so nothing complains, and the error is in the content.
The reference existence check
The technique has an unglamorous name: a reference existence check. Whenever a model emits something that points at your code, resolve the pointer against the actual code before you believe it.
There are only a few kinds of reference, and each has a cheap resolver.
A file path. Does the file exist on disk? This is a single stat call.
import fs from "node:fs";
function fileExists(repoRoot, path) {
return fs.existsSync(new URL(path, repoRoot));
}
A symbol. The model says a function, class, or export is named X. Grep for its definition. A definition-shaped match (an export function X, a class X, a def X) is stronger evidence than a bare mention, so prefer a pattern that looks for the declaration.
# does this symbol get defined anywhere?
rg -n "\\b(function|class|const|def)\\s+processRefund\\b" src/
An import or dependency. The model claims module A uses module B. Parse A's imports and look. You do not need a full compiler for this. A lightweight parse of the import statements, or even a scoped grep, resolves most cases.
A config or environment key. The model references BILLING_RETRY_LIMIT. Search the config files and the schema. If it is not declared, the model made it up or renamed it.
The pattern is always the same. The model asserts a reference, deterministic code resolves it, and if it does not resolve, you stop and flag it rather than passing it downstream. This is deterministic validation of an LLM claim, and deterministic is the operative word: the same input always gives the same verdict, with no second model to second-guess.
Where to put the check
A resolver is only useful if it runs at the right moment. Three places earn their keep.
At the boundary of a tool call. If your agent calls a tool that takes a file path, validate the path inside the tool before it does any work. A tool that refuses to operate on a nonexistent file turns a silent hallucination into a clean, catchable error at the exact moment it happens.
Between plan and action. When a model produces a plan that names files, functions, and modules, resolve every reference in the plan before executing any of it. A plan that points at three real files and one imaginary one is a plan you want to reject and regenerate, not half-run.
As a gate on structured output. If your model returns JSON with fields like file, symbol, or module, run the resolvers as part of validating that object, right after you check the schema. Schema validation confirms the shape. The resolver confirms the shape points at something real.
The theme across all three: check references as close as possible to where they enter your system, so a false one never gets to masquerade as a true one downstream.
Make the failure useful, not just loud
Catching the hallucination is half the job. The other half is turning it into a signal the model can act on. When a reference fails to resolve, do not just throw a generic error. Tell the caller what was claimed, that it did not resolve, and, when it is cheap to compute, what real references are nearby.
A message like billing/retry.ts not found; nearest matches: billing/queue.ts, billing/retryPolicy.ts gives a model or a human enough to correct course on the next pass. This turns a dead end into a retry with a hint, which is usually all it takes. Good grounding is not only about retrieving the right context up front, a topic covered in RAG for code; it is also about failing informatively when a claim slips through.
The honest limitation
A reference existence check proves a pointer resolves. It does not prove the pointer is the right one. The model can name a real file that has nothing to do with the task. It can reference a function that exists but does the wrong thing. Those are correctness questions, and correctness is harder and more expensive to judge than existence.
So treat this as the floor, not the ceiling. It is the cheap first pass that removes an entire category of obvious, damaging errors for almost no cost, which frees your more expensive checks, tests, judgment, human review, to focus on the errors that actually require thought. What it will not do is make an agent trustworthy on its own. Reliability comes from a stack of these checks, each catching what the last one cannot, described more fully in the pillar on making reliable AI coding agents.
This is one of the first checks we run inside Loopsfinity, because it is the rare guardrail that is both trivial to build and genuinely load bearing. When an agent works against a real codebase, the fastest way to lose trust is to act confidently on a file that was never there. The fastest way to keep it is to look, every time, before you believe.