teachyou.ai academy
← All posts
Codex

OpenAI Codex for Bug Fixing: A Systematic Workflow

Ira Menon · May 20, 2026 · 14 min read

Why Most Developers Use Codex Wrong for Bug Fixes

Open a terminal, paste a stack trace into Codex, type "fix this," and hope. That's how most developers use OpenAI Codex CLI for debugging, and it's why so many of them end up with a patch that silences the error message without touching the actual defect. The model is genuinely capable of finding and fixing bugs, but it needs the same thing a human engineer needs: a reproducible case, enough context to understand the system, and a way to verify the fix actually worked.

The difference between a frustrating Codex session and a productive one is almost never about the model's raw capability. It's about the workflow around it. A junior engineer handed a bug ticket with no repro steps, no logs, and no acceptance criteria will flail. Codex flails too, just faster and with more confidence in its wrong answers. Give it a tight reproduction, relevant file context, and a clear definition of "fixed," and it goes from a coin flip to a reliable tool you can point at your bug backlog.

This article lays out a systematic, repeatable workflow for using Codex CLI to fix bugs — one that treats the model like a capable but literal-minded collaborator rather than an oracle. We'll cover reproduction, context gathering, prompting patterns, sandboxed execution, diff review, and regression prevention, with real command examples throughout.

Step 1: Reproduce the Bug Before You Touch Codex

The single biggest predictor of whether Codex fixes a bug correctly is whether you can reliably reproduce it first. If you can't reproduce the failure, Codex can't verify its own fix, and neither can you. This isn't a Codex-specific rule — it's just good debugging discipline — but it matters more with an AI agent because the agent will happily generate a plausible-looking patch for a bug it never actually observed.

Before opening Codex, spend five minutes turning the bug report into an executable reproduction:

  • A failing test case, even a rough one
  • A minimal script or CLI command that triggers the error
  • The exact input that causes bad output (a malformed payload, a specific user ID, a race condition trigger)

If the bug is intermittent — a flaky test, a race condition, a memory leak that only shows up under load — write down what you know about the failure rate and conditions. Codex can work with "fails roughly 1 in 20 runs under concurrent writes," but it can't work with "sometimes it just breaks."

A concrete example. Suppose a Python service occasionally returns stale cache data after a write:

# repro_stale_cache.py
import asyncio
from myapp.cache import CacheLayer
from myapp.store import Store

async def reproduce():
    store = Store()
    cache = CacheLayer(store)

    await store.write("user:42", {"name": "Alice"})
    await cache.invalidate("user:42")

    # Simulate a concurrent read racing the invalidation
    result = await asyncio.gather(
        cache.get("user:42"),
        store.write("user:42", {"name": "Alice-Updated"}),
    )
    print("Cache returned:", result[0])
    # Bug: sometimes prints the OLD value even after write completes

asyncio.run(reproduce())

Running this script five or ten times and noting the failure rate gives Codex something concrete to reason about, and gives you a script it can re-run after any proposed fix.

Step 2: Gather Context Before Prompting

Codex CLI runs inside your repository and can read files on its own, but it doesn't know which files matter unless you point it there or give it enough of a trail to follow. Before writing your prompt, gather:

  • The exact error message or stack trace, unedited
  • The file(s) where the failure originates, if you have a hunch
  • Recent related commits (git log -p -- path/to/file) if the bug is a regression
  • Any relevant tests that currently pass but shouldn't, or that don't exist yet

A fast way to hand Codex a regression's history is to check what changed recently in the suspect area:

git log --oneline -20 -- src/cache/
git log -p --since="2 weeks ago" -- src/cache/invalidation.py

If the bug was introduced by a specific commit, git bisect combined with your reproduction script is one of the most reliable ways to narrow the search space before Codex even gets involved:

git bisect start
git bisect bad HEAD
git bisect good v2.3.0
git bisect run python repro_stale_cache.py

Once bisect identifies the offending commit, you can hand that commit hash directly to Codex as part of the prompt. This turns "find the bug somewhere in a 50,000-line codebase" into "explain why this specific 12-line diff causes stale reads," which is a dramatically easier and more reliable task for the model.

Step 3: Write a Bug-Fixing Prompt That Constrains the Model

Vague prompts produce vague fixes. "Fix the cache bug" invites Codex to guess at scope, invent a root cause, and potentially rewrite far more than necessary. A good bug-fixing prompt for Codex CLI includes four things: the observed behavior, the expected behavior, the reproduction method, and explicit scope boundaries.

Here's a prompt structure that works well in practice:

Bug: Cache returns stale data after a write, roughly 1 in 5 runs.

Reproduction: run `python repro_stale_cache.py` — watch for
"Cache returned: {'name': 'Alice'}" instead of the updated value.

Suspect area: src/cache/invalidation.py, changed in commit a3f9c1d
("optimize invalidation batching").

Expected: cache.get() should never return a value older than the
most recent completed write for that key.

Constraints:
- Do not change the public CacheLayer API.
- Do not introduce a global lock; this path is called at high concurrency.
- Add or update a test that fails before your fix and passes after.
- Keep the diff scoped to invalidation.py and its tests unless you
  find the root cause lives elsewhere — explain if so before editing.

Notice what this prompt does NOT do: it doesn't tell Codex what the fix should be. It tells Codex what "fixed" means (the expected behavior) and what "acceptable" means (the constraints). This is the same distinction between a bug ticket and a design doc — you want Codex diagnosing and proposing, not blindly executing a solution you half-guessed at from the terminal.

Launch this inside Codex CLI's approval workflow so you can review before anything is written to disk:

codex --approval-mode suggest "$(cat bugfix_prompt.txt)"

Using suggest mode (or the equivalent review-before-apply flag in your installed version) means Codex proposes the diff and explains its reasoning, but doesn't touch files until you approve. For bug fixes — where an overly aggressive automated edit can be worse than the original bug — this extra checkpoint is worth the few seconds it costs.

Step 4: Let Codex Investigate Before It Edits

A pattern that consistently improves Codex's bug-fixing accuracy: ask it to explain the bug before asking it to fix the bug. Splitting investigation from remediation into two separate turns forces the model to commit to a theory of the failure, which you can sanity-check before any code changes happen.

First, without writing any code, read src/cache/invalidation.py and
src/cache/store.py and explain the likely root cause of the stale-read
bug described above. Reference specific line numbers. Do not propose
a fix yet.

Read the explanation carefully. This is the single highest-leverage moment in the whole workflow — if Codex's stated root cause is wrong, any fix built on top of it will also be wrong, no matter how clean the resulting diff looks. Common failure modes to watch for in the model's explanation:

  • It blames a plausible-sounding but unrelated piece of code because that's where similar bugs "usually" live
  • It correctly identifies a symptom but not the underlying cause (e.g., blaming a missing await instead of the race condition that missing await exposes)
  • It assumes framework or library behavior that isn't actually true for the version pinned in your requirements.txt or package.json

If the explanation looks right, move to the second turn:

That matches what I'm seeing. Now implement a fix. The batching
optimization in invalidation.py appears to defer cache-key deletion
until a flush interval, which lets a read slip in between write and
delete. Fix this without removing the batching optimization entirely —
we need it for throughput. Update or add tests in tests/test_cache.py.

This two-step pattern costs one extra round trip but catches a meaningful fraction of misdiagnoses before they turn into wasted edits.

Step 5: Run Codex in a Sandbox for Anything Destructive

Bug fixes sometimes require exploratory changes — deleting a cache, resetting a database migration, truncating a log file to isolate an issue. Never let an agent run this kind of exploration against a shared or production-adjacent environment. Codex CLI's sandbox and approval flags exist precisely for this:

codex --sandbox workspace-write --approval-mode auto-edit \
  "Reproduce the stale-cache bug, then iterate on a fix in
   src/cache/invalidation.py until repro_stale_cache.py passes
   10 consecutive runs with no stale reads."

workspace-write scopes filesystem writes to the project directory, and pairing it with a loop-until-passing instruction lets Codex iterate autonomously on a well-defined, mechanically verifiable goal — rerunning the reproduction script itself, reading the failure, adjusting, and rerunning again. This loop is where Codex earns its keep on bug fixing: it's the same tedious edit-run-observe cycle a human does, just faster and without losing patience on run 8 of 10.

For anything that touches a real database, external API, or production configuration, run the session inside a container or disposable VM rather than trusting sandbox flags alone. A Docker Compose stack with a disposable Postgres instance is usually enough:

docker compose -f docker-compose.test.yml up -d
codex --sandbox workspace-write "Reproduce and fix the migration
  bug against the test database at localhost:5433. Do not touch
  any other environment."
docker compose -f docker-compose.test.yml down -v

Step 6: Review the Diff Like You're the One Deploying It

Codex will hand back a diff and, usually, a summary of what it changed and why. Read the diff itself, not just the summary — the summary is the model's narrative about its own work, and narratives can be more flattering than the code they describe.

Three things to check on every bug-fix diff before accepting it:

  1. Scope. Did the fix stay within the files and boundaries you specified? An agent that "fixes" a null-pointer bug by adding defensive null checks in six unrelated files is masking problems, not solving them.
  2. Root cause vs. symptom. Does the diff address the mechanism you and Codex agreed on in Step 4, or does it just catch the exception and move on? A try/except: pass wrapped around the failing line will make your reproduction script stop printing errors without fixing anything.
  3. Test quality. If Codex added a test, does it actually exercise the failure condition, or does it just call the function once with happy-path inputs? Temporarily revert the fix and confirm the new test fails against the old code — this single check catches a surprising number of tests that pass regardless of whether the bug exists.
git stash
python -m pytest tests/test_cache.py -k stale_read -v
git stash pop

If the test passes even with the fix stashed away, it isn't testing the bug — send Codex back with that specific finding.

Step 7: Verify Against the Original Reproduction, Not Just Unit Tests

Unit tests are necessary but not sufficient. The reproduction script from Step 1 is your ground truth because it's the thing that actually demonstrated the bug in the first place, independent of whatever test Codex wrote. Always close the loop by running it again after the fix lands:

for i in $(seq 1 20); do python repro_stale_cache.py; done

If the bug was intermittent, running the reproduction a meaningful number of times (not just once) matters — a race condition that failed 1 in 20 times before the fix should be run at least 20-30 times after the fix before you trust it's gone. A single clean run tells you very little about a probabilistic bug.

For bugs involving performance regressions or resource leaks, pair this with a lightweight measurement rather than relying on pass/fail alone:

python -m memory_profiler repro_stale_cache.py

Step 8: Prevent the Bug From Coming Back

A bug fix that doesn't leave a trace in your test suite is a bug that will reappear, possibly reintroduced by a future Codex session that doesn't know the history. Before closing out the fix, make sure three things exist:

  • A regression test committed alongside the fix, with a name that references the actual failure mode (test_cache_get_does_not_race_write_invalidation, not test_cache_2)
  • A comment at the fix site explaining the non-obvious constraint, especially if the fix looks unnecessary in isolation (future engineers, human or AI, will "simplify" code they don't understand the history of)
  • An entry in your commit message describing root cause, not just symptom, so git blame and git log tell the real story later
def invalidate(self, key: str) -> None:
    # Deferred/batched deletion previously allowed a read to slip in
    # between a write and its cache invalidation (see issue #482).
    # We now mark the key as "pending invalidation" synchronously and
    # check that marker on read, even though actual deletion still
    # happens on the batch flush interval.
    self._pending.add(key)
    self._batch.append(key)

This is also where it's worth asking Codex a follow-up question you should always ask after any fix: "are there other call sites in this codebase with the same pattern that could have the same bug?" Codex CLI can grep across the whole repo far faster than you can by hand, and catching three latent instances of the same defect in one session is a much better outcome than fixing them one bug ticket at a time over the next six months.

codex "Search the codebase for other places that read from a batched
  or deferred invalidation cache without checking a pending-invalidation
  marker, similar to the bug just fixed in invalidation.py. List file
  and line for each, don't edit anything yet."

Common Pitfalls That Derail Codex Bug-Fixing Sessions

A few failure patterns show up often enough to call out directly:

  • Accepting the first diff without running the repro script. The diff looking clean and the diff actually fixing the bug are two different claims. Only one of them matters.
  • Letting Codex "fix" flaky tests by adding retries or increasing timeouts. This is the single most common way an intermittent bug gets papered over instead of fixed. If Codex proposes a retry loop or a longer sleep as the fix, push back and ask for the actual race condition or timing assumption underneath it.
  • Skipping the investigation step under time pressure. It's tempting to jump straight to "fix it" when you're trying to close a ticket fast, but skipping Step 4 is exactly when Codex is most likely to patch a symptom.
  • Not scoping the sandbox. Running an autonomous fix-and-verify loop against a real database or live API because it was faster than spinning up a disposable one. It works right up until it doesn't.
  • Treating the model's confidence as a signal. Codex will describe a wrong root cause with the same fluent, assured tone as a correct one. Confidence is not evidence — the reproduction script is evidence.

Building This Into a Repeatable Habit

None of these steps are exotic. They're the same discipline good engineers already apply when debugging without AI assistance: reproduce first, understand before you edit, scope your changes, verify against the original failure, and leave a trail for next time. What changes with Codex CLI is the speed at which you can move through that loop — reproduction, hypothesis, fix, verification, and regression-proofing can happen in minutes instead of hours, provided you don't skip steps to chase that speed.

The teams that get the most out of Codex for bug fixing treat it less like a magic "fix my bug" button and more like a very fast, very literal pair programmer that needs the same inputs a human collaborator would need: a clear repro, relevant context, and an honest description of what "done" looks like. Do that consistently, and Codex turns from a coin-flip tool into one of the more reliable parts of your debugging toolkit.

If you want hands-on practice building this exact workflow — sandboxed execution, prompt patterns for investigation versus remediation, and real bug-fixing exercises against a live codebase — check out the OpenAI Codex CLI Tutorial course on teachyou.ai, where we walk through this process end-to-end on real, messy bugs rather than toy examples.