Codex /goal only stops when you tell it how to check: write the stop condition as a script
Goal mode keeps Codex working across turns until it believes the objective is met. A 20-line check script makes that belief verifiable.

A migration agent has one failure mode that matters more than the rest: it says it’s finished. Four of the five files are on the new HTTP client, the fifth still imports the old one, and the summary reads “Migration complete”. Nobody notices until a deploy.
Codex’s goal mode makes that worse and better at the same time. Worse, because the agent now runs for hours across many turns without you reading each one. Better, because the feature is built around a stopping condition, and a stopping condition can be a command that exits non-zero. This piece shows how to write one for a migration and wire it into /goal, so “done” is something Codex has to demonstrate rather than assert.
The docs are explicit about this design. OpenAI says goal mode gives Codex one durable objective and keeps it working across turns toward a verifiable stopping condition, and that Codex stops when it’s confident it has reached it. Confidence is the weak point. The fix is to give it something to be confident about.
What goal mode is, in commands
Goal mode left experimental status on 21 May 2026 and is available in the Codex app, the IDE extension and the CLI, according to the Codex changelog. The commands, from the slash command reference:
/goal <objective>sets a goal/goalshows the current one/goal editrevises the objective/goal pause,/goal resumeand/goal clearcontrol the run
The goal stays attached to the active chat. If /goal doesn’t appear in the slash menu, the use-case page says to enable it in config.toml:
[features]
goals = true
or run codex features enable goals. The slash command page doesn’t mention a feature flag at all, so if one of the two instructions doesn’t work in your version, try the other.
One hard limit: an objective must be non-empty and at most 4,000 characters. For anything longer, the docs say to put the detail in a file and point the goal at it.
The problem with a prose stopping condition
OpenAI’s starter prompt is:
/goal Complete [objective] without stopping until [verifiable end state].
The weak spot is the second blank. “Until the migration is finished” is not a verifiable end state; it’s the agent’s opinion. “Until all tests pass” is better, but a migration can pass every test while three modules still use the old library, because nothing tests which import a file uses.
A good end state has two properties. It’s measurable by a command, not by reading the agent’s summary. And it fails loudly in the specific way the task usually goes wrong. For a library migration, that means counting what’s left, not just checking that what’s there still works.
The check script
Here’s a stop condition for moving a TypeScript project from legacy-http to http-next. It’s a helper script rather than application code, so Python is the simplest thing to drop in:
#!/usr/bin/env python3
"""Stop condition for the legacy-http migration. Exit 0 only when it is really done."""
import pathlib, subprocess, sys
root = pathlib.Path(__file__).parent
test_cmd = sys.argv[1:] or ["npx", "vitest", "run"]
legacy = sorted(p for p in root.glob("src/**/*.ts") if "legacy-http" in p.read_text())
print(f"files still importing legacy-http: {len(legacy)}")
for p in legacy:
print(f" {p.relative_to(root)}")
tests = subprocess.run(test_cmd, cwd=root)
print("tests:", "pass" if tests.returncode == 0 else "FAIL")
done = not legacy and tests.returncode == 0
print("GOAL MET" if done else "GOAL NOT MET")
sys.exit(0 if done else 1)
Two things make this work as a stop condition. The output names the remaining files, so a failing run is also a to-do list: Codex doesn’t have to rediscover what’s left. And the exit code is the verdict, so there’s nothing to interpret.
Here are three runs: everything migrated, everything migrated but the tests failing, and one file still on the old import:
$ python3 goal-check.py true
files still importing legacy-http: 0
tests: pass
GOAL MET
exit 0
$ python3 goal-check.py false
files still importing legacy-http: 0
tests: FAIL
GOAL NOT MET
exit 1
$ python3 goal-check.py true
files still importing legacy-http: 1
src/UsersClient.ts
tests: pass
GOAL NOT MET
exit 1
Passing true and false as the test command is only there to show both branches. In a real project, leave the argument off and the script runs npx vitest run. For a C# solution, swap the default for ["dotnet", "test"] and change the glob and the search string to match .cs files and the old namespace.
Setting the goal
Per the prompting guide, the recommended order is /plan first, so Codex investigates and proposes an approach before it edits anything, then /goal once the plan looks right. The goal text then carries the stop condition and the checkpoints the docs ask for:
/goal Migrate every file under src/ from legacy-http to http-next, following
the plan above. Do not stop until `python3 goal-check.py` exits 0. Run it
after each file you migrate and after any change to shared code. Keep a
running log in MIGRATION_LOG.md: file, what changed, check output. If the
check can't pass without a product decision, stop and write the question
at the top of the log.
Each sentence is doing a job:
- The command is named, so the end state isn’t open to interpretation.
- “After each file” turns the script into a progress meter rather than a final exam, which is how the use-case page frames it: commands or artifacts that prove progress.
- The log file is the checkpoint artifact. It’s also what you read when you come back, instead of scrolling a long transcript.
- The escape clause matters. The docs’ own prompt-optimisation example tells Codex to stop when the target is met or when further changes would need product or policy guidance. Without a way out, an agent that hits an ambiguity will either guess or loop.
If the goal text outgrows 4,000 characters, move the rules into the plan or a MIGRATION.md and write /goal Follow MIGRATION.md; done when goal-check.py exits 0.
Watching a run
/goal with no arguments shows where things stand. Pause with /goal pause when you want to look at the diff without Codex editing underneath you, and /goal resume carries on. If you notice the objective was wrong (say, the new client has a different retry default you’d rather settle first), /goal edit revises it without starting over.
Two habits are worth building. First, run the check script yourself before you trust a “done”. It takes seconds and it’s the same command Codex ran. Second, open git diff --stat before you read any code: a migration that touched forty files when the check listed five is a signal to look closer.
Variations
For a refactor with no library to count, pick a different measure. Rename a method across a codebase and the remaining-count is a search for the old name. Split a 2,000-line class and the measure can be a line-count ceiling per file plus the existing tests.
For a performance goal, make the script print the number and compare it with a threshold. The docs’ own example of iterating on prompts until an eval suite hits a target pass rate is the same shape: a command that prints a score and exits non-zero below the bar.
If the work spans several repositories, keep the script in one place and call it by absolute path from each, so every goal ends on the same definition of done.
What goes wrong
The script only guards what it measures. A search for legacy-http misses a dynamic require, a re-export through a barrel file, or the old package left in package.json. Add the checks you’d add to a code review: the dependency is gone from the manifest, the lockfile is regenerated, no new any casts crept in to quiet the compiler.
Tests can be weakened to reach the exit code. An agent working toward “tests pass” has an incentive to edit the test. The goal text should say that test files are read-only for this task, and the check script can enforce it:
changed = subprocess.run(["git", "diff", "--name-only", "HEAD", "--", "tests/"],
capture_output=True, text=True).stdout.split()
if changed:
print("tests were modified:", *changed)
sys.exit(1)
That snippet is untested here, so run it once by hand before relying on it. It blocks legitimate test updates as well, which is the point: if the migration really needs a test change, that’s the human decision the escape clause is for.
Cost is the other limit. The use-case page says a goal can run independently for several hours, and every one of those turns uses tokens. The /goal docs describe no token budget, so don’t assume a cap. Watch the first run, and prefer a small, bounded migration to start with.
Finally, goal mode is the wrong tool for a loose list of unrelated tasks. The use-case page says to avoid exactly that. If you can’t write one command that says the work is done, you don’t have a goal yet, you have a backlog.
First 15 minutes
Pick a migration you’re already planning, such as a library swap or a rename across a module. Write the check script for it first and run it by hand, so you see it fail on today’s code. Then start Codex, run /plan, and set the goal with the script named as the only acceptable finish.