Codex Fixes One Bug But Breaks Nearby Logic

The reported bug is gone, two adjacent features regressed. Cap blast radius, enumerate call sites, prefer caller-side guards over shared-util changes.

You filed: “fix the timezone bug in the booking flow.” Codex’s PR makes the bug go away — and silently changes how formatDate() handles null, which is called from 14 other places. The booking flow works; the email service now ships emails with 1970-01-01 because the old contract (display null as ”—”) quietly changed.

TL;DR (the fastest fix): before you approve the patch, run git diff --stat and look for any file under lib/, utils/, or common/ in the diff. If a shared utility changed but only one feature’s tests are green, do not merge. Then re-prompt Codex with two clauses that prevent ~80% of this: (1) “edit only files under [target path]; if the fix needs a shared util, STOP and list every caller first,” and (2) “produce a consumer-impact report before applying.” For a harder, non-optional guardrail, scope Codex at the sandbox level instead of trusting prompt text — see Step 0.

The first instinct is “Codex broke things.” The actual root cause is unbounded blast radius: Codex was free to change shared code, didn’t audit consumers, and you didn’t ask it to. The fix is constraining changes by scope, requiring a consumer-impact report before merge, and preferring caller-side guards over shared-util mutations.

Which bucket are you in

Symptom after the patchMost likely causeJump to
A shared util (lib/, utils/, common/) is in git diff --statFix touched widely-called codeCause 1
Same return type, different runtime value ("" vs null)Semantics changed, signature didn’tCause 2
Callers now throw on inputs that used to workNew precondition / validation addedCause 3
A downstream read (cache, queue, event) stopped firingA side effect was removedCause 4
Bug’s own test passes; a neighbor feature breaks in prodTests covered the fix, not the neighborsCause 5
typecheck errors in files Codex never openedCode moved, imports brokeCause 6

Common causes

Ordered by hit rate, highest first.

1. Fix touched a shared utility called from many places

formatDate, parseUser, serializeError, getEnv — utilities used everywhere. A “small fix” inside ripples to every consumer, half of which Codex didn’t read.

How to spot it: git diff --stat after Codex’s patch. If any utility / common file is in the diff but only one feature’s tests are passing, you’re missing the impact on the others.

2. Codex changed function semantics without changing the signature

The function still returns string, but now returns "" instead of null for missing input. Type-system thinks nothing changed; callers that did if (result === null) silently break.

How to spot it: Look for changes inside function bodies that alter return values, throw behavior, or side effects without changing the type signature. These are TypeScript-invisible breaks.

3. Patch added a new precondition / validation

getUser(id) previously accepted any string; Codex added “if id doesn’t match UUID regex, throw.” Old callers passing numeric IDs (“/users/42”) now crash.

How to spot it: New throw statements at the top of functions are red flags. Audit every caller for input shape.

4. Codex changed a side effect

saveSession(user) used to also write to Redis. Codex “cleaned it up” by removing the Redis write because “the function name says save, not cache.” Now session loads from cache fail.

How to spot it: Removed lines that did I/O (write to DB, queue message, emit event) inside a function whose name doesn’t obviously imply that I/O.

5. Tests covered the fix but not the neighbors

Codex added a regression test for the original bug; the test passes. But there’s no test for the email service that depends on the same util, so the regression is invisible until production.

How to spot it: Look at the test diff. If the only new test exercises the fixed path, the surrounding paths are untested.

6. The fix moved code between files, breaking imports elsewhere

Codex extracted a helper to lib/helpers.ts. Some files imported it from its old location (utils/format.ts); the moved version isn’t reexported. Type errors land in unrelated places.

How to spot it: pnpm typecheck reports errors in files not in Codex’s diff. The new errors are import-resolution failures.

Shortest path to fix

Ordered by ROI. Steps 1 and 2 prevent 80% of collateral damage. Step 0 is the only one Codex cannot ignore.

Step 0: Use the sandbox, not just the prompt

Prompt text is a request, not a fence — as the Codex docs themselves note, “there is no guarantee that guidelines inside this file will be followed.” The firm controls are sandbox_mode and approval_policy. As of June 2026, Codex CLI runs --sandbox workspace-write by default, which already keeps writes inside the current workspace and makes .git, .codex, and .agents read-only at the OS level regardless of what the model “decides.”

That default does not stop Codex from rewriting a shared util that lives inside the workspace — but you can shrink the workspace. Launch Codex from the narrowest directory that still contains the bug, so lib/ and utils/ sit outside its writable root:

# Run Codex scoped to the feature, not the whole repo.
# Writes outside this directory now require an explicit approval prompt.
cd src/features/booking
codex --sandbox workspace-write --ask-for-approval on-request "fix the timezone bug"

With --ask-for-approval on-request (the default that pairs with --full-auto), any attempt to touch a path outside the launch directory pauses and asks you first — so a shared-util edit surfaces as a prompt instead of a silent diff. Avoid --full-auto from the repo root for fix tasks, and never use --yolo / --dangerously-bypass-approvals-and-sandbox here: it disables both the sandbox and approvals, which is exactly the blast radius you are trying to cap.

Step 1: Constrain blast radius in the prompt

Task: Fix [specific bug] in [specific file/feature].

CONSTRAINTS:
- You may edit ONLY files in [target path].
- DO NOT change any shared utility (lib/, utils/, common/) without explicit approval.
- DO NOT change function signatures or return contracts.
- If the fix requires touching a shared util, STOP and list every caller first.

The “stop and list” clause is the critical lever — it forces Codex to surface the blast radius before acting.

Step 2: Require a consumer-impact report before applying

For any change touching a shared file:

Before applying the patch, produce this report:
1. List every file that imports the function/symbol you changed:
   grep -rn "import.*formatDate" --include="*.ts" --include="*.tsx" .
2. For each caller, describe in one sentence:
   - What input shape it passes
   - What return value it expects
   - Whether your change preserves that contract
3. Flag any caller whose contract is broken — fix or escalate.

Only after the report passes, apply the patch.

Step 3: Prefer caller-side guards over shared-util changes

If the booking flow needs formatDate to handle null differently, don’t change formatDate — change the booking caller:

// Bad — changes shared util, blast radius = all callers
function formatDate(d: Date | null): string {
  if (!d) return "—"; // changed
  return d.toISOString();
}

// Good — caller-side guard, blast radius = booking only
function renderBookingDate(d: Date | null): string {
  if (!d) return "—";
  return formatDate(d);
}

Rule of thumb: if the new behavior is feature-specific, the guard belongs in the feature.

Step 4: Run a wider test net than the bug’s own tests

In the prompt:

After applying, run not only the bug's test but:
- The full test file for any imported util you changed
- Every test file matching grep for the changed symbol

Report pass/fail count for each.

For a monorepo:

# Find tests that import any changed file
git diff --name-only origin/main -- 'src/**' | while read f; do
  symbol=$(basename "$f" .ts)
  grep -rln "from.*${symbol}" --include="*.test.ts" --include="*.spec.ts" . | sort -u
done

Step 5: Authorize shared-util changes deliberately

When a shared change really is needed, make the authorization explicit:

You may change `lib/format.ts` for this task.
Before applying:
1. List every consumer (`grep -rn "from.*lib/format"`).
2. For each, show me the line that uses the changed export.
3. Wait for my approval before applying.

This makes the cross-cutting change feel like a refactor (deliberate, reviewed) instead of a fix sneaking in extra scope.

Step 6: Add a “consumers” test layer for hot utilities

For utils used in >5 places, add a test file that exercises each consumer’s contract:

// lib/format.consumers.test.ts
import { formatDate } from "./format";

describe("formatDate consumers", () => {
  it("booking flow: handles null as '—'", () => {
    // booking-specific expectation
    expect(formatDate(null, { fallback: "—" })).toBe("—");
  });
  it("email service: handles null as empty string", () => {
    expect(formatDate(null, { fallback: "" })).toBe("");
  });
});

If Codex later changes formatDate, this file fails immediately and pinpoints which consumer’s contract broke.

Prevention

  • Default rule in AGENTS.md: “Do not change shared utilities (lib/, utils/, common/) without enumerating callers”
  • For any patch touching a shared file, require a consumer-impact report before merge
  • Prefer caller-side guards over modifying shared utils — keeps blast radius local
  • Test files for hot utils should include consumer-contract tests, not just unit tests
  • Run wider-than-strictly-necessary tests after shared changes: full feature suites, not just the bug’s own test
  • Treat shared-util edits as refactors, not fixes — separate PR, separate review, deliberate authorization

How to confirm it’s actually fixed

Don’t trust “the bug’s test is green.” Confirm the blast radius is zero:

  1. git diff --stat origin/main — confirm no file under lib/, utils/, or common/ changed unless you authorized it in Step 5.
  2. Run the full test suite, not the single bug test: pnpm test (or npm test / pytest). A passing target test with a failing neighbor is the exact failure mode this article is about.
  3. pnpm typecheck (or tsc --noEmit) — catches Cause 6 (moved imports) and any caller that relied on a now-changed return type.
  4. For every shared symbol that did change, grep its callers and eyeball one line each: grep -rn "formatDate" src/. If a caller compares against null, "", or a specific string, verify the new behavior still satisfies it.
  5. If you have a staging environment, smoke-test at least one neighbor feature (here: send one email) before merge — Cause 5 only shows up at runtime.

FAQ

Why does this keep happening when my prompt clearly said “only fix the booking bug”? Because the prompt is advisory. The model can still decide a shared util is the cleanest fix. The only non-bypassable limit is the sandbox: launch Codex from the feature directory (Step 0) so anything outside it triggers an approval prompt instead of a silent edit.

Does --sandbox workspace-write stop Codex from editing shared utilities? Only if those utilities live outside the writable root. The workspace-write sandbox blocks writes outside the launch directory and protects .git, .codex, and .agents, but a lib/ folder inside the same workspace is still fair game. Shrink the workspace (cd into the feature) to push shared code out of reach.

Codex changed a function’s return value but TypeScript shows no errors. Why? The signature didn’t change — it still returns string (Cause 2). Switching null to "", or adding a throw, is invisible to the type system because the static type is identical. These breaks only surface through tests or runtime, which is why Steps 4 and 6 widen the test net beyond the bug’s own test.

Is this a Codex bug I should report? Usually no — it’s a scoping gap on the caller’s side. Report it only if Codex wrote outside the workspace sandbox without asking (that has been filed before, e.g. openai/codex issue #11583). Otherwise the fix is constraining scope, not waiting on a patch.

What’s the single highest-leverage prevention? A default rule in AGENTS.md: “Do not change shared utilities (lib/, utils/, common/) without enumerating callers first.” It won’t be obeyed 100% of the time (guidelines aren’t guarantees), so pair it with the Step 0 sandbox scoping and a git diff --stat check on every patch.

Tags: #Codex #Coding agent #Troubleshooting #Debug #Collateral break