teachyou.ai academy
← All posts
Codex

OpenAI Codex Prompting Tips for Better Results

Ira Menon · May 17, 2026 · 16 min read

Why your Codex prompts keep producing mediocre code

You open a terminal, type something like "fix the bug in the checkout flow," and hit enter. Codex thinks for a while, edits three files, and hands you a diff that technically runs but doesn't actually match what you meant. You didn't get a bad model — you got a bad prompt.

Codex is only as effective as the instructions you give it. That single idea is the difference between developers who treat Codex like a magic autocomplete and developers who treat it like a very capable but very literal junior engineer who has zero memory of your codebase's unwritten rules. The junior engineer framing matters: Codex doesn't know your team hates default exports, doesn't know your staging database has fake data, and doesn't know "the bug" you're referring to unless you tell it. It will happily guess, and its guesses are often plausible-looking and wrong.

The good news is that prompting Codex well is a learnable skill, not a mysterious art. It comes down to a handful of habits: giving Codex the right amount of context, being explicit about what "done" looks like, breaking big asks into steps it can verify, and pushing durable rules out of your prompt and into project configuration so you're not repeating yourself every session. This article walks through each of those habits with concrete before-and-after prompt examples you can start using today, whether you're driving Codex from the CLI, the IDE extension, or the cloud interface.

The four-part prompt structure that fixes most bad results

Most weak Codex prompts are missing one of four ingredients: Goal, Context, Constraints, and Done when. Think of these as the four questions Codex is silently asking itself before it starts editing. If you don't answer them, it fills in the gaps with assumptions.

  • Goal — the outcome you want, described as a result, not a method. "Make login faster" is a goal. "Add a cache" is a method that may or may not achieve the goal.
  • Context — which files, folders, error messages, or examples are relevant. Codex can read your whole repo, but pointing it at the right five files beats letting it wander through five hundred.
  • Constraints — the standards, conventions, and boundaries it must respect: which libraries are off-limits, which architecture pattern to follow, what NOT to touch.
  • Done when — the verifiable condition that proves the task is finished. A test that passes, a command that runs clean, a page that loads under a target time.

Here's the same request written badly and then well.

Bad prompt:

Fix the pagination bug.

Good prompt:

Goal: Fix the pagination bug where page 2 of /api/orders returns
duplicate rows from page 1.

Context: The bug is in src/routes/orders.ts, in the getOrders
handler. It uses OFFSET/LIMIT pagination against the orders table.
See the failing test in tests/orders.pagination.test.ts.

Constraints: Don't change the API response shape. Keep using
Postgres OFFSET/LIMIT (no cursor-based rewrite in this task).
Follow the error-handling pattern already used in routes/users.ts.

Done when: tests/orders.pagination.test.ts passes, and running
`npm run test:integration` shows no regressions.

Notice the second prompt isn't longer because it's padded with pleasantries — it's longer because it's carrying actual information. Codex can now reproduce the bug, knows exactly which file to open first, knows what it's not allowed to change, and knows precisely how to check its own work before handing the diff back to you. That last part — self-checking — is what separates a one-shot lucky fix from a reliable one.

You won't need all four elements for a trivial one-line change. But for anything that touches business logic, more than one file, or a subtle behavior, skipping one of these four elements is usually where things go wrong.

Write goals as outcomes, not instructions

A common mistake is prompting Codex the way you'd write a step-by-step ticket for yourself, when Codex often does better with a clearly stated outcome and room to figure out the mechanism.

Weaker:

Add a Redis cache in front of the getUserProfile function.

Stronger:

Goal: Reduce the average response time of GET /api/profile below
150ms under normal load. It currently averages 400-600ms because
every request re-fetches from Postgres.

Context: src/services/userProfile.ts, load test results in
perf/profile-load-test.json.

Constraints: Any caching layer must invalidate on profile updates
(see updateUserProfile in the same file). Prefer solutions that
don't add a new infrastructure dependency unless nothing else works.

Done when: perf/profile-load-test.json re-run shows p95 latency
under 150ms, and updating a profile immediately reflects in a
follow-up GET request (no stale reads).

The outcome-first version leaves Codex free to decide whether Redis, an in-memory LRU cache, or a query optimization is the right fix — and because you specified the constraint about staleness, it won't hand you a cache that silently serves old data. If you already know you want Redis specifically because of an infra decision, say so as a constraint, not as the whole goal. The distinction matters: goals describe success, constraints describe the boundaries around how you get there.

Give Codex the right context, not all the context

It's tempting to think "more context is always better" and just tell Codex to "look at the whole repo." In practice, dumping too much undifferentiated context dilutes the signal. Codex has to spend its attention figuring out what's relevant instead of solving your problem.

Too vague:

Here's my whole checkout module, please add support for gift cards.

Better scoped:

Goal: Add support for applying a single gift card code at checkout.

Context:
- src/checkout/CartSummary.tsx (where the price breakdown renders)
- src/checkout/applyDiscount.ts (existing discount-code logic —
  gift cards should follow the same validation flow)
- src/api/giftCards.ts (already has a validateGiftCard(code) function
  you should reuse, don't reimplement validation)

Constraints: Gift cards can only be applied once per order and
cannot be combined with percentage discounts (see the existing
rule in applyDiscount.ts around combinable discount types).

Done when: A new gift card test in
tests/checkout/giftCard.test.ts passes, covering: valid code,
invalid code, and attempting to combine with a percentage discount.

This version names exact files and even points out a function to reuse instead of reinvent — a small detail that prevents Codex from duplicating validation logic that already exists elsewhere in the codebase. When you know a helper, a pattern, or a prior implementation already solves part of the problem, say so explicitly. Codex is good at reading code you point it to; it's not psychic about which of your fifteen "discount" files matters this time.

If you're working in the Codex IDE extension, it automatically includes your currently open files and selected text range as context, which is a convenient shortcut for smaller edits — but for cross-file work, naming files explicitly in the prompt is still more reliable than assuming the ambient context is enough.

State your constraints before Codex writes a line of code

Constraints are the most commonly skipped part of a prompt, and it's the one that causes the most rework. Developers often only add constraints after they've already rejected a first attempt — "no, don't use a class component," "no, we don't do inline styles here." You can save that entire round trip by stating constraints upfront.

No constraints stated (expect a rewrite):

Build a settings page for notification preferences.

Constraints stated upfront:

Goal: Build a settings page where users can toggle email, SMS,
and push notification preferences independently.

Context: src/pages/settings/ for existing settings page patterns,
src/components/Toggle.tsx for the toggle component we already use
everywhere else.

Constraints:
- Use the existing Toggle component, don't build a new one.
- Follow the settings page layout in src/pages/settings/Privacy.tsx
  (same header, same save-button placement).
- State management goes through useSettingsStore, not local
  component state — see how Privacy.tsx does it.
- No new npm dependencies.

Done when: New page matches the visual pattern of Privacy.tsx,
saves preferences via useSettingsStore, and a test confirms toggling
one preference doesn't affect the other two.

Constraints aren't about limiting Codex's creativity — they're about encoding the tribal knowledge that lives in your team's heads but not in the code itself. If your team always puts business logic in hooks instead of components, or never touches a specific legacy file without a migration plan, that's a constraint worth stating every time, or better yet, worth writing down once so you never have to state it again — which is exactly what AGENTS.md is for.

Stop repeating yourself: put durable rules in AGENTS.md

If you find yourself typing the same constraint into every prompt — "use TypeScript strict mode," "run npm run lint before you're done," "we use pnpm not npm," "never touch the legacy/ folder" — that's a signal the rule belongs in an AGENTS.md file, not in your prompt.

AGENTS.md is effectively an open-format README written for Codex instead of for humans. Codex reads it automatically before starting work, at both a global level (your Codex home directory) and a repository level, with closer, more specific files taking precedence over global defaults. This means you can set broad personal defaults once and let individual repositories override them with project-specific norms.

A solid repository-level AGENTS.md typically documents:

  • Repository structure and where key logic lives
  • Build, test, and lint commands (exact commands, not "run the usual tests")
  • Engineering conventions (naming, folder structure, preferred patterns)
  • Hard constraints (files or directories that are off-limits, dependencies that are banned)
  • How to verify a change is correct (which command output means success)

A minimal example:

# AGENTS.md

## Setup
- Package manager: pnpm (never npm or yarn)
- Install: `pnpm install`
- Run dev server: `pnpm dev`

## Testing
- Unit tests: `pnpm test`
- Full check before declaring a task done: `pnpm lint && pnpm test`

## Conventions
- All new components go in src/components, one folder per component
- State management: Zustand stores in src/stores, no Redux
- No default exports — always named exports

## Do not touch
- legacy/ — frozen code pending a separate migration project
- src/payments/ — requires a review from the payments team

Once this file exists, your prompts get shorter and more focused, because you no longer need to restate the rules every single time — you're only stating what's different about this particular task. The advice here is to build AGENTS.md based on mistakes Codex has actually made, not mistakes you imagine it might make. If Codex never tries to use npm instead of pnpm, you don't need a rule for it. Update the file when you notice a repeated correction, and it becomes a living record of your team's norms.

Break big tasks into steps Codex can verify

Codex handles complex work better when it's broken into smaller, focused steps, largely because smaller tasks are easier for Codex to test — and easier for you to review. A single enormous prompt asking for a full feature end to end tends to produce a large diff that's hard to review carefully, and if something's wrong, it's harder to pinpoint which part of the change caused it.

One giant ask:

Build a full user referral system: referral codes, tracking,
reward payouts, an admin dashboard, and email notifications.

Broken into reviewable steps:

Step 1
Goal: Generate a unique referral code for each user on signup.
Context: src/models/User.ts, src/services/auth/signup.ts.
Constraints: Codes are 8 characters, alphanumeric, must be unique
across all users.
Done when: A new user always gets a code, and a test confirms two
signups never collide (run 1000 simulated signups in the test).
Step 2 (after step 1 is merged)
Goal: Track when a new user signs up using someone else's referral
code.
Context: src/services/auth/signup.ts (already updated in step 1),
new table needed for referral_events.
Constraints: Don't reward the referrer yet — this step only records
the relationship.
Done when: Signing up with a valid referral code creates a
referral_events row linking referrer and referee.

Each step has its own goal, context, and done-when condition, and each one produces a diff small enough to actually read before merging. If step 2 introduces a bug, you know exactly where to look, because step 1 was already verified and merged. This is also where it helps to ask Codex to propose the breakdown itself if you're not sure how to split the work — describe the full feature, then ask "propose a step-by-step plan before writing code," and review the plan before letting it start.

Ask Codex to prove its own work

A prompt that ends the moment code is written is an incomplete prompt. Don't stop at asking Codex to make a change — ask it to create tests when needed, run the relevant checks, confirm the result, and review its own work before you look at it. Codex can absolutely do this loop, but only if your prompt tells it what "good" looks like.

Prompt with no verification step:

Add input validation to the signup form.

Prompt with a built-in verification loop:

Goal: Add input validation to the signup form (email format,
password minimum 10 characters, matching confirm-password field).

Context: src/components/SignupForm.tsx.

Constraints: Use the existing validation helper pattern in
src/utils/validators.ts rather than a new validation library.

Done when:
1. Write unit tests for each validation rule (valid email rejected
   vs accepted, short password rejected, mismatched confirm-password
   rejected).
2. Run `pnpm test SignupForm` and confirm all tests pass.
3. Run `pnpm lint` and confirm no new warnings.
4. Summarize which edge cases you tested and which you didn't.

That fourth point in the done-when list — asking for a summary of what was and wasn't tested — is cheap to add and extremely useful. It forces Codex to be explicit about coverage gaps instead of implying the feature is bulletproof. You'll often learn about an edge case you hadn't thought of, like what happens when the password field contains only whitespace, simply because Codex flags it as untested.

If you're fixing a bug rather than adding a feature, include the reproduction steps directly in the prompt. "Run pnpm dev, go to /checkout, add two items, apply a 10%-off code, refresh the page — the discount disappears" gives Codex a concrete failure to reproduce and then verify is fixed, rather than a description it has to interpret.

Match reasoning effort to the task, and use planning mode for ambiguity

Codex supports different reasoning levels, and picking the right one for the task avoids two failure modes: wasting time on trivial changes, or under-thinking genuinely hard ones.

  • Low reasoning — fast, well-defined tasks like renaming a variable across a file, adding a straightforward null check, or writing a small utility function with an obvious implementation.
  • Medium or high reasoning — complex changes, multi-file refactors, or debugging a failure whose root cause isn't obvious yet.
  • Extra-high reasoning — long, reasoning-heavy agentic tasks, such as diagnosing a flaky production issue across several services or planning a non-trivial migration.

If you're not sure how to scope a task at all — the request is big, ambiguous, or you genuinely don't know the right approach — use planning mode before asking for code. Planning mode has Codex gather context and ask clarifying questions first, producing a plan you can review and adjust before a single line of code is written. This is far cheaper than reviewing a large, wrong diff after the fact.

Skipping planning on an ambiguous ask:

Migrate our REST API to GraphQL.

Using planning first:

I want to migrate our REST API (src/routes/) to GraphQL over time,
without breaking existing consumers during the transition.
Before writing any code, propose a step-by-step plan: what should
move first, how we run REST and GraphQL side by side, and what
the rollback looks like if a step goes wrong.

The second version gets you a reviewable plan first — a sequence of small, verifiable steps, each with its own done-when condition — instead of an enormous, unreviewable diff attempting the entire migration in one pass. You can then feed each step of that plan back to Codex individually, using the four-part structure from earlier.

Common mistakes that quietly wreck good prompts

A few habits show up repeatedly in weak Codex sessions, and all of them are easy to fix once you notice them:

  • Hardcoding durable rules into every prompt instead of moving them into AGENTS.md. If you're typing "remember, we use pnpm" for the tenth time, that's a config problem, not a prompting problem.
  • Describing the method instead of the goal, then getting frustrated when Codex's method doesn't match the one in your head. State the outcome, and put your preferred method in constraints if it's non-negotiable.
  • Skipping verification criteria and being surprised the diff "works" but doesn't actually solve your problem. If you don't say what "done" looks like, Codex has to guess, and its guess is whatever makes the code compile.
  • One giant prompt for a multi-part feature. Large diffs are hard to review and hard to debug when something's subtly wrong. Split it, verify each piece, then move to the next.
  • Running multiple live Codex sessions against the same files without isolating them in separate branches or worktrees, which leads to conflicting edits landing on top of each other.
  • Accumulating unrelated tasks in a single long thread. Context quality degrades as a thread fills with unrelated work; start a fresh thread per coherent task instead of tacking a new request onto an old one.
  • Over-permissioning immediately. Start with tighter approval and sandbox settings, and loosen them deliberately once you understand how Codex behaves in your specific repo, rather than granting broad access on day one out of impatience.

None of these are exotic mistakes — they're the prompting equivalent of not writing a clear ticket. The fix is almost always the same: say the outcome, point at the real files, name the boundaries, and define what proof of success looks like.

Putting it together: a repeatable prompting habit

The pattern underneath everything in this article is small enough to memorize: Goal, Context, Constraints, Done when. Say what you want as an outcome. Point Codex at the specific files and prior art that matter. State the rules it must respect. Define exactly how it — and you — will know the work is actually finished. Layer in AGENTS.md for anything you'd otherwise repeat, break large asks into steps that each get verified before you move to the next, and reach for planning mode whenever you're not fully sure how the work should be sequenced.

None of this requires memorizing special syntax or magic keywords. It requires treating Codex the way you'd treat a sharp new engineer joining your team: give them the context a stranger would need, tell them what success looks like, and ask them to show their work. Do that consistently, and the gap between "technically ran" and "actually correct" mostly disappears.

If you want to go deeper — structuring AGENTS.md for real production repos, wiring up MCP integrations, building reusable Skills, and running multi-step agentic workflows with confidence — that's exactly what we cover, hands-on, in the OpenAI Codex CLI Tutorial course on teachyou.ai, where you'll practice these exact prompting patterns on real codebases instead of toy examples.