Codex Makes Unsafe Assumptions

Codex uses a column, env var, or function signature it never read. Force a quote-before-write rule and run Codex read-only so it grounds every line in your real files.

Codex wrote user.handle in a SELECT, but the column is user.username. Or it called sendEmail(to, subject, body) when the signature is sendEmail({ to, subject, html }). Or it hard-coded https://api.example.com when your project reads the URL from process.env.API_URL. The code looks right. It doesn’t compile, doesn’t run, or worse, runs against the wrong target.

Fastest fix: add one rule to the top of your prompt (or AGENTS.md): “Before you write code that touches an external function, schema column, or env var, read its source file and quote the exact line in your reply. If you can’t quote it, stop and ask.” This single rule catches most unsafe assumptions because it forces Codex to open the file instead of guessing from a training-time prior.

The underlying problem is “confident hallucination”: Codex inferred a shape from patterns it has seen instead of reading your actual file. Without grounding, even capable models guess. The rest of this page is the full diagnosis and the per-target prompts (schema, signature, env var) plus how to verify the result.

Which bucket are you in

Match the symptom to the most likely cause, then jump to the fix.

Symptom in the generated codeMost likely causeFirst checkFix
Wrong column / table nameCodex never opened the schemaDid the transcript show a read of schema.prisma?Step 2
Wrong argument order / shapeInferred signature from imports or a popular librarySearch transcript for a read of the definition fileStep 1, Step 3
Wrong env var name or typeInvented the name; treated a string as a numberDiff against .env.exampleStep 4
Symbol that doesn’t exist at allPure hallucinationgrep for the definitionStep 5
Correct-looking but historically wrongStale AGENTS.md or JSDocgit log -1 the doc vs the sourceStep 6

Common causes

Ordered by hit rate, highest first.

1. Codex didn’t open the file, inferred from imports

The prompt named a file, but Codex never actually opened it. It guessed the function’s shape from how it’s imported elsewhere or from its training-time prior. Arguments are subtly wrong.

How to spot it: Run /status in the Codex TUI to see which directories are in scope, then scroll the transcript for a tool call that reads the relevant file. If there’s no read of that file, Codex worked from imagination. (In workspace-write mode Codex can read your files automatically — but only if it chooses to, and only inside the workspace boundary. Naming the file in your prompt is not the same as forcing a read.)

2. Stale AGENTS.md documenting a past reality

Your AGENTS.md says the users table has handle. Six months ago you renamed it to username. The doc wasn’t updated; Codex trusted it. This matters more than it used to: Codex concatenates AGENTS.md files root-first, leaf-last, up to project_doc_max_bytes (32 KiB by default as of June 2026), so a stale root file feeds Codex wrong facts on every single turn.

How to spot it: Grep AGENTS.md for the wrong symbol Codex used. If it’s there, the doc is stale.

sendEmail(to, subject, body) is the canonical Node.js shape from countless blog posts. Your project uses an options-object variant. Codex defaulted to the popular shape.

How to spot it: The wrong signature matches an obvious open-source library’s shape. Codex picked the common pattern, not your code.

4. Schema lives in a generated file Codex didn’t read

Prisma’s schema.prisma is the source of truth, but the generated node_modules/.prisma/client/index.d.ts is where the TypeScript types live. Codex read neither and fell back to guessing.

How to spot it: Wrong types or columns where Codex should have used Prisma’s generated types. Check whether Codex’s reads include schema.prisma or the generated .d.ts.

5. Env var format invented

You document STRIPE_KEY=sk_test_.... Codex used STRIPE_API_KEY (extra word). Or it wrote process.env.PORT as a number directly, but env vars are always strings, and your code needs parseInt or Number().

How to spot it: Compare Codex’s env var names and shapes against .env.example. Mismatches are usually invented.

6. Codex over-trusted a comment / JSDoc that’s out of date

The function has a JSDoc saying @param userId: string, but the implementation now accepts number | string. Codex trusted the doc, called it with a string, and it ran fine — until another caller passes a number and the type system doesn’t catch the mismatch.

How to spot it: The implementation signature differs from the JSDoc. The JSDoc is the lie; the implementation is the truth.

Shortest path to fix

Ordered by ROI. Step 1 alone catches most unsafe assumptions. A useful trick before you start: switch Codex to read-only with /permissions (or launch with --sandbox read-only) for the planning phase, so it has no choice but to read and quote before it can write anything.

Step 1: Quote-before-write rule

In every code-generating prompt:

Before writing any code that calls an external function, schema, or env var:

1. Read the file where it's defined.
2. Quote the relevant lines verbatim in your reply (signature, columns, exports).
3. ONLY THEN write the code that uses it.

If you cannot quote it (file not found, doesn't compile), STOP and ask.
Do not infer from imports or training-time priors.

This adds one extra step but eliminates the “I assumed it looked like X” failure mode.

Step 2: For schemas, demand the schema read first

You're writing a query against `users`.
First: read `prisma/schema.prisma` and quote the `User` model verbatim.
Then: write the query, using ONLY columns that appear in the quoted model.
If a needed column doesn't exist, propose adding it (do not assume).

For DB-less environments, point at the migration file or the raw DDL. If you use Prisma, also tell Codex it may read the generated node_modules/.prisma/client/index.d.ts for exact types.

Step 3: For function signatures, demand the file read first

You're calling `sendEmail`.
First: read `src/lib/email.ts` and quote the export signature.
Then: write the call, matching that exact signature.

If Codex can’t quote it, the file may not exist; surface that mismatch before any code lands.

Step 4: For env vars, anchor to .env.example

You're using an env var.
1. Read `.env.example` and quote the relevant lines.
2. Use the exact name from `.env.example` — do not invent variants.
3. Remember env values are strings; cast with parseInt/Number if a number is needed.
4. If the var isn't in `.env.example`, propose adding it.

Step 5: After Codex writes, verify with grep

Don’t trust the report. Check that referenced symbols actually exist:

# For each unique identifier in the new code, grep for its definition
grep -rn "function sendEmail\|export const sendEmail\|export function sendEmail" src/
grep -rn "model User\|interface User" prisma/ src/

Empty results mean the symbol Codex used doesn’t exist. Then let the compiler and a real run finish the job: npx tsc --noEmit flags wrong shapes that grep can’t, and a quick test run catches env vars that exist but hold the wrong value.

Step 6: Refresh stale AGENTS.md before relying on it

If your AGENTS.md is older than the code it describes and the codebase changes a lot, it’s lying to Codex. Audit:

# When did key docs last update vs key files?
git log -1 --format="%ai" AGENTS.md
git log -1 --format="%ai" prisma/schema.prisma
git log -1 --format="%ai" src/lib/email.ts

If the schema or email file changed after AGENTS.md, refresh AGENTS.md or remove the stale section so Codex reads the source of truth instead. Because Codex caps the concatenated AGENTS.md chain at 32 KiB and reads root-first, keep the root file lean — a bloated global file can crowd out the directory-specific instructions that are actually accurate.

How to confirm it’s fixed

  1. The reply quotes real lines. A grounded answer shows the actual signature/columns/env lines from your files before the generated code. No quote means it guessed again.
  2. grep finds every referenced symbol (Step 5). Zero hits anywhere means a hallucinated identifier slipped through.
  3. The type checker is clean. npx tsc --noEmit (or your language’s equivalent) passes with no “property does not exist” or argument-count errors.
  4. It runs against the right target. Confirm any URL or key the code uses resolves to the env var, not a hard-coded literal.

Prevention

  • Make “quote before write” a permanent AGENTS.md rule, not a per-prompt instruction.
  • Name the source-of-truth file for schemas, env vars, and function signatures directly in AGENTS.md.
  • Treat generated types (Prisma, GraphQL codegen) as truth and point Codex at them.
  • Keep the root AGENTS.md lean so it doesn’t hit the 32 KiB cap and crowd out accurate nested files; audit it quarterly and delete sections that no longer match the code.
  • For risky planning, run Codex in read-only mode (/permissions) so it must read before it can write.
  • After Codex writes, grep the referenced symbols; if any are missing, it hallucinated.
  • Confidence in AI is uncorrelated with correctness. Verify with the compiler and grep instead of trusting the tone of the reply.

FAQ

Does naming the file in my prompt force Codex to read it? No. In workspace-write mode (the default “Auto” sandbox) Codex can read files in the workspace, but it decides whether to. Naming a file is a hint, not a guarantee. The quote-before-write rule turns the hint into a hard requirement because the reply is incomplete without the quoted lines.

Will read-only mode stop Codex from hallucinating entirely? No, but it changes the failure mode usefully. In read-only mode Codex can’t edit, so it produces a plan you review before any code lands. It still has to read and quote to ground that plan, which is exactly the grounding you want. Switch back to workspace-write to apply the change.

Codex insists a column exists that I know was removed. Why? Almost always a stale doc. Grep AGENTS.md and any JSDoc for the old name, then check git log -1 on the doc versus the schema. If the schema changed later, the doc is feeding Codex an outdated fact on every turn until you fix it.

The env var name is right but the value is wrong at runtime. What now? That’s not an assumption bug; the var exists. Check that you cast it (parseInt(process.env.PORT)), that .env and .env.example agree on the name, and that the process actually loaded .env (Codex’s sandbox doesn’t inject your shell’s environment by default).

How do I see what Codex actually read? Scroll the TUI transcript for the file-read tool calls, and run /status to confirm which directories are in scope. If the file you care about was never read and isn’t in scope, that’s your root cause.

External references: Codex agent approvals & security and the Codex configuration reference (for project_doc_max_bytes and sandbox modes).

Tags: #Codex #Coding agent #Troubleshooting #Debug #Unsafe assumptions