Skip to content
OTFotf
All posts

Why Coding Agents Hallucinate in Sandboxes: The Real Fix

D
DaveAuthor
7 min read
Why Coding Agents Hallucinate in Sandboxes: The Real Fix

A coding agent in a browser sandbox is doing improv comedy with no audience to ground it. Every prompt is a cold open. The model doesn't know your folder structure, your import patterns, your naming conventions, or whether Button lives in src/components/ui/ or app/_components/. So it guesses. Then it guesses again. Then it writes a Button that imports from ./ui/button.tsx that doesn't exist, hallucinates a Card API, and ships a <div> dressed as a Tabs.

Anyone who's pasted a "build me a SaaS dashboard" prompt into a browser-based vibe-coding tool has seen this. The first 200 tokens of output look great. By token 800 the agent has invented a useAuthStore that returns { user, login, logout, register, sendMagicLink } — none of which match whatever you actually have on disk, because there's no disk. By token 2000 you're holding 47 hallucinated files in a tree that won't compile in any environment you control.

The reason is structural. A sandbox strips away the three things an agent needs to stay accurate: a stable filesystem to re-read, conventions encoded in existing code, and a persistent rules file that survives sessions. Without them, the context window fills with the model's own previous guesses, and the next prediction is grounded in nothing.

a sandboxed agent's context window is full of its own previous guesses — a real repo makes

The mechanism: what sandboxes strip away

A coding agent's job is to predict the next token given a context window. Its accuracy is bounded by what's in that window. In a real repo, the window contains:

  1. The actual files it just read — paths, existing imports, current dependencies, real exports.
  2. A persistent AGENTS.md / CLAUDE.md / .cursorrules with repo-specific conventions.
  3. Build and test output — feedback that corrects the next prediction.

In a browser sandbox, none of those survive across prompts. The filesystem is an in-memory tree the agent itself wrote. There's no real package.json to check — the agent regenerates one from memory each turn. The "conventions file" is either absent or reset every session. The model isn't hallucinating out of malice. It's hallucinating because the context window is full of its own previous guesses, with no ground truth to anchor them to.

Three failure modes I've watched happen

Path drift. Turn 1: src/components/Button.tsx. Turn 4: app/_components/Button.tsx. Turn 7: components/Button/index.tsx. Same component, three different paths, none of them real by the time the agent tries to import one.

API drift. Turn 1 exports Button({ variant, size }). Turn 3 wants to use it, but the agent has now "remembered" it as Button({ kind, scale, theme }). The previous exports exist only in the conversation history, not on a filesystem the agent can re-read.

Dependency hallucination. import { useFormState } from 'react-dom' — a hook that didn't exist until React 19, casually dropped into a React 18 codebase. Or import { z } from 'zod/v4' — a future-major that has been "announced" in the model's training but not shipped.

Each one is a small mistake. They compound. By turn 10 the agent is debugging its own hallucinations.

One codebase. iOS, Android, and web.

The Fitness Kit ships with auth, a database, and a backend already connected — no setup. Live demo at fitness-preview.otf-kit.dev.

See the live demo

What a real repo fixes

A real, owned repo gives the agent three things a sandbox can't:

  • A stable filesystem to re-read. The agent can grep import { Button } and find every callsite. It can read package.json and pin the real React version. The context window stops being a memory palace and starts being a queryable surface.
  • Convention encoded in code, not in chat. When Button.tsx already exists with a clean variant API, the next time the agent touches a button it reads the file. The convention is the file. The agent doesn't have to remember — it just has to read.
  • A persistent rules file that survives sessions. AGENTS.md, CLAUDE.md, .cursorrules, README.md. One file, in the repo, read on session start. Anything you want the agent to never guess at — naming, structure, forbidden patterns, test commands — goes here.

Convention beats configuration, every time

Configuration asks the model to remember. Convention lets the model re-derive. The latter is cheaper, more accurate, and survives model swaps.

If every *.test.ts lives next to its source file, the agent reads Button.tsx, sees Button.test.tsx next to it, and writes the next test in the same place — without you telling it. If your forms use a stable useZodForm.ts hook in src/lib/, the agent reads that, copies the pattern, and the new form passes lint on the first try.

The rule: encode the convention in a file the agent will definitely read, not in a prompt it might forget.

How to actually set this up today

Here's a minimal CLAUDE.md that makes a measurable dent in agent hallucination:

# Repo conventions

## Layout
- Components: `src/components/<Name>/index.tsx`
- One component per folder. Co-locate `*.test.tsx` next to source.
- Server logic: `src/server/<domain>/<action>.ts`

## Imports
- Use the `@/` alias. Never relative imports deeper than one level.
- UI primitives come from `@otfdashkit/ui`. Don't write your own Button, Card, or Dialog.

## TypeScript
- Strict mode. No `any`. No `@ts-ignore`. If you need it, justify in a comment.
- Prefer `type` over `interface`. Discriminated unions for state.

## Testing
- Vitest. `pnpm test` runs all. `pnpm test <pattern>` runs one file.
- New components ship with a render test and a primary interaction test.

## Forbidden
- Don't add deps without a one-line justification in the diff.
- Don't introduce a state library — we use React's built-ins.
- Don't scaffold a Button, Card, or Dialog — read `@otfdashkit/ui` first.

That's it. ~30 lines. Claude Code, Cursor, and most modern agents auto-load this on session start. Every guess that file kills is a hallucination you don't have to debug at 11pm.

For Cursor users, the same content goes in .cursorrules at the repo root. For others, check what your tool auto-loads; the file name is the only difference.

A second lever: scripts that read themselves

Conventions in prose are good. Conventions in code are better. A repo can ship a tiny script the agent runs before making structural decisions:

// scripts/check-conventions.ts
// run: pnpm tsx scripts/check-conventions.ts
import { readdirSync, statSync } from 'node:fs'
import { join } from 'node:path'

const ROOT = 'src/components'
const violations: string[] = []

for (const entry of readdirSync(ROOT)) {
  const p = join(ROOT, entry)
  if (statSync(p).isDirectory()) {
    if (!readdirSync(p).includes('index.tsx')) {
      violations.push(`${p}: missing index.tsx`)
    }
  }
}

if (violations.length) {
  console.error('Convention violations:')
  violations.forEach(v => console.error('  -', v))
  process.exit(1)
}
console.log('OK')

Add it to the conventions file:

Before scaffolding a new component, run `pnpm tsx scripts/check-conventions.ts`.
If it exits non-zero, fix the violations first.

Now the agent can't accidentally regress the layout. The repo enforces its own conventions, and the agent reads the enforcement as data, not as chat history.

Where OTF fits

Claude Code is genuinely good at long-horizon refactors when it has a real repo to read. Cursor's inline-edit loop is the fastest way to ship a UI tweak that crosses three files. Use them.

The piece that doesn't change when you swap tools is the repo itself. A kit that ships with CLAUDE.md, .cursorrules, and ~20 tested prompts already wired into the layout gives every agent — Claude Code, Cursor, Aider, whatever's next — the same grounding on day one. The agent reads the real filesystem. The conventions are encoded in the actual components. The forbidden patterns are listed in a file that survives every session. Hallucination drops because guesswork drops.

That's the part worth investing in: a repo so well-grounded that any agent can extend it without first having to re-derive it.

Closing

The fix isn't a smarter model. The fix is a more readable repo. Sandboxes strip away the three things agents need most — a stable filesystem, conventions encoded in code, and a persistent rules file. A real, owned repo puts them back. The 30-line conventions file and one enforcement script will save you more hallucinated components than any prompt-engineering trick.

ai-toolsarchitecturereact-native
OTF Fitness Kit

Stop wiring. Start shipping.

  • Login, database, and backend already connected — nothing to set up
  • iOS + Android + web from one codebase
  • AI configs pre-tuned + 40+ tested prompts included