An AI agent telling you "done" is not evidence.
When I started delegating work to Codex, I took those reports at face value — until I checked the code and found the change missing, the wrong file edited, or no commit at all.
So I stopped trusting language and started making the shell write the facts to disk.
Why this design works When you hand a task to Codex, it comes back with "Completed." At first that satisfied me.
But when I actually checked the code, the critical change wasn't there, or a different file had been touched, or had never run.
The output "I did it" and the fact "it was actually done" are two different things.
This is true of Claude Code too.
Whether tool results were read correctly, whether errors were swallowed — even with code I wrote myself, running a self-audit right after declaring completion turns up something every single time.
Delegating implementation to an AI amplifies that problem by one more notch.
The fix is simple: make it write state to a file, not to language.
Even if the AI says "completed," it isn't complete unless exists in the status file.
If the handoff file doesn't contain the real output of , you don't know what changed.
If the four sections you specified in the task file (Summary, Files Changed, Validation, Remaining Risks) aren't there, you can't verify it.
Files don't lie.
An AI under pressure will insist "I did it," but the output of can't be forged.
Pushing state management down into the filesystem is what makes it possible for a human to cross-check it in a shell.
That's the essence of this design.
The other important piece is separation of concerns. orchestrate-codex-worker.sh takes three arguments up front.
Each of these three files has a clear role. task-file: The work order for Codex.
It contains only "what to do." handoff-file: The handoff note after Codex finishes.
Written on success or failure alike, on the assumption that the next person in line (the next Claude Code session, or me) will read it. status-file: Machine-readable progress state.
It takes only three values: → / .
What happens without this separation?
When instructions and execution results live in the same place, "is this an instruction or post-execution output?" becomes ambiguous.
When you run large numbers of tasks in parallel, that ambiguity is fatal.
Multiple Codex workers can run in the same directory without interfering as long as each has its own independent task/handoff/status files.
On top of that, the script starts with .
That's a declaration that "the entire script stops the moment any command fails." Without it, Bash ignores errors and moves to the next line.
With , the behavior becomes: if git rev-parse fails, stop; if mkdir fails, stop.
A design that doesn't swallow errors matters especially in a script built on the premise of AI delegation.
The overall flow Following the script's behavior in order looks like this.
Looking at the function — the core of the actual code — shows what's being recorded. returns the branch name at that moment. is the absolute path of the worktree.
The timestamp comes out as UTC ISO 8601 via .
In other words, the status file tells you line by line when, on which branch, in which worktree, and in what state.
When running in parallel across multiple worktrees, just looking at the Worktree field in the status file identifies which is which.
The prompt handed to Codex is also assembled directly inside the script.
Two things stand out. "Do not write handoff or status files yourself" — an explicit prohibition.
Who writes the handoff/status files is a key design fork.
If you let Codex write them, Codex may output something that merely looks right.
Writing them from the script side means you get the actual output of , the actual branch name from , and the actual time from .
Those can't be falsified.
Forcing four sections.
Requiring "exactly these sections" in Codex's output fixes the positions that downstream processing and review will reference.
Open the handoff file, read the "Files Changed" section, and you have the list of changed files; read the "Validation" section and you have the verification commands Codex actually ran and their results.
With free-form output, when the next session loads the handoff file you no longer know where to look.
The codex invocation itself is one line. means no confirmation prompts, specifies the model, sets the working directory, sets the output destination, and tells it to read the prompt from stdin. keeps ANSI escape sequences from contaminating the handoff file, so no junk characters get in the way when you grep or parse it later.
Whether this call succeeds (exit 0) or fails (non-zero) decides the branch that follows.
Because the script has , a failing codex command doesn't exit outright — the structure routes it into the error branch.
Even on failure, the handoff file and status file are always written.
Never producing a state of "no file = it was never even run" is what the later cross-check verification requires.
Implementation details What protects There's a mechanism I didn't cover in the first half that you should read first. creates a temp file like . declares "when the script exits — whether exit 0 or exit 1 — run the function." Why is this needed? reads and writes to , but Codex sometimes exits non-zero.
When Codex fails in a environment, the script enters the else block and ends with exit
1.
Without , would linger.
Once is fine, but run 30 workers in parallel and bloats.
With , the temp files disappear no matter which path exits.
One more point: note that and are global variables.
At definition time, the function doesn't know the contents of / .
It reads the variable values at exit, when the function runs.
That's exactly why the order is: assign the variables right after , then set the trap.
Reverse the order and cleanup tries to delete empty paths and errors out.
Why the task-file existence check comes before "running" Reading the script, the task-file existence check comes before . means "submission to Codex has begun." If the task file can't be read, the premise for submitting to Codex has collapsed, so it isn't entitled to be "running." It writes directly and exits
1.
The reason comes first is the same.
When the task file can't be read and you try to write failed, if the directories for the handoff file or status file don't exist, that write itself fails.
Make sure the parent directories exist before any write to the handoff/status files.
That's why sits at the top.
What the handoff file holds on success When Codex's exit code is 0, the following gets written to the handoff file. pulls in Codex's entire output.
Immediately after, and follow.
That is the linchpin of verification.
Suppose Codex wrote "Files Changed: src/api/index.ts, tests/api.test.ts" in its Summary — if shows nothing, that means git doesn't recognize any change to those files.
The cross-check commands I actually use are these.
If the handoff file's and agree, what Codex said matches git's actual state.
If they don't, it's one of two things: "Codex thought it made changes but actually didn't," or "it went all the way through commit, so nothing showed in (= clean)." For the latter, I check the in the status file and trace it with .
The reason for passing to Codex lies here.
If ANSI escape sequences (control characters like or ) get into output_file, they're transcribed verbatim into the handoff file.
When you run , invisible control characters break the pattern match. is the instruction "don't include ANSI codes in output," and without it, mechanical post-processing gets contaminated.
Don't assume "nothing changed anyway" on failure The handoff file on failure is minimal.
Just the single line Codex's output is partially written to , but is not executed here.
Why?
The on failure is incomplete output.
The four sections may not all be there.
An API error may have hit partway through, leaving everything from Summary onward unwritten.
Mixing incomplete output into the handoff file means t