cd /news/ai-agents/make-codex-prove-it-a-three-file-des… · home topics ai-agents article
[ARTICLE · art-115439] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Make Codex Prove It: A Three-File Design That Leaves Evidence on Disk

A developer has created a three-file design to verify AI agent work, addressing the problem of agents like Codex and Claude Code claiming completion without actually making changes. The system uses separate task, handoff, and status files, with the status file recording state transitions and the handoff file containing real git output, so a human can cross-check results in a shell. The approach emphasizes separation of concerns and error handling with set -euo pipefail.

read25 min views1 publishedAug 30, 2026

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.

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 git commit

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 State: completed

exists in the status file. If the handoff file doesn't contain the real output of git status --short

, 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 cat status-file

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.

bash scripts/orchestrate-codex-worker.sh <task-file> <handoff-file> <status-file>

Each of these three files has a clear role.

State: running

State: completed

/ State: failed

.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 set -euo pipefail

. 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 set -euo pipefail

, 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.

Following the script's behavior in order looks like this.

呼び出し元(Claude Code / cronジョブ等)
    │
    ├─ task-file を渡す(作業指示)
    ├─ handoff-file のパスを渡す(引き継ぎ先)
    └─ status-file のパスを渡す(状態管理先)
         │
         ▼
orchestrate-codex-worker.sh
    │
    ├─ [起動直後] write_status "running"
    │        └→ status-file: State: running / Branch / Worktree / timestamp
    │
    ├─ task-file の読み込み確認
    │   ├─ [失敗] write_status "failed" + handoff-fileにエラー書き出し → exit 1
    │   └─ [成功] 処理継続
    │
    ├─ mktemp で prompt_file / output_file を作成
    │   └─ trap cleanup EXIT(終了時に自動削除)
    │
    ├─ prompt_file を組み立て(Codexへの指示 + task-fileの内容)
    │
    ├─ codex exec -p yolo -m gpt-5.4 -C $(pwd) -o output_file < prompt_file
    │   │
    │   ├─ [成功]
    │   │     handoff-file に書き出し:
    │   │       - Completed: timestamp
    │   │       - Branch: git rev-parse --abbrev-ref HEAD
    │   │       - Worktree: pwd
    │   │       - output_file の内容(Summary/Files Changed/Validation/Remaining Risks)
    │   │       - git status --short
    │   │     write_status "completed"
    │   │
    │   └─ [失敗]
    │         handoff-file に書き出し:
    │           - Failed: timestamp / Branch / Worktree
    │           - "The Codex worker exited with a non-zero status."
    │         write_status "failed" → exit 1
    │
    └─ 完了

Looking at the write_status

function — the core of the actual code — shows what's being recorded.

write_status() {
  local state="$1"
  local details="$2"

  cat > "$status_file" <<EOF

- State: $state
- Updated: $(timestamp)
- Branch: $(git rev-parse --abbrev-ref HEAD)
- Worktree: `$(pwd)`

$details
EOF
}

git rev-parse --abbrev-ref HEAD

returns the branch name at that moment. $(pwd)

is the absolute path of the worktree. The timestamp comes out as UTC ISO 8601 via date -u +"%Y-%m-%dT%H:%M:%SZ"

.

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.

cat > "$prompt_file" <<EOF
You are one worker in an ECC tmux/worktree swarm.

Rules:
- Work only in the current git worktree.
- Do not touch sibling worktrees or the parent repo checkout.
- Complete the task from the task file below.
- Do not spawn subagents or external agents for this task.
- Report progress and final results in stdout only.
- Do not write handoff or status files yourself; the launcher manages those artifacts.
- If you change code or docs, keep the scope narrow and defensible.
- In your final response, include exactly these sections:
  1. Summary
  2. Files Changed
  3. Validation
  4. Remaining Risks

Task file: $task_file

$(cat "$task_file")
EOF

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 git status --short

, the actual branch name from git rev-parse --abbrev-ref HEAD

, and the actual time from timestamp

. 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.

codex exec -p yolo -m gpt-5.4 --color never -C "$(pwd)" -o "$output_file" - < "$prompt_file"

-p yolo

means no confirmation prompts, -m gpt-5.4

specifies the model, -C "$(pwd)"

sets the working directory, -o "$output_file"

sets the output destination, and - < "$prompt_file"

tells it to read the prompt from stdin. --color never

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 set -euo pipefail

, a failing codex command doesn't exit outright — the if codex exec ...; then ... else ... fi

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.

trap cleanup EXIT

protects There's a mechanism I didn't cover in the first half that you should read first.

prompt_file="$(mktemp)"
output_file="$(mktemp)"
cleanup() {
  rm -f "$prompt_file" "$output_file"
}
trap cleanup EXIT

mktemp

creates a temp file like /tmp/tmp.XXXXXX

. trap cleanup EXIT

declares "when the script exits — whether exit 0 or exit 1 — run the cleanup

function."

Why is this needed? codex exec

reads prompt_file

and writes to output_file

, but Codex sometimes exits non-zero. When Codex fails in a set -euo pipefail

environment, the script enters the else block and ends with exit 1. Without trap

, /tmp/tmp.XXXXXX

would linger. Once is fine, but run 30 workers in parallel and /tmp

bloats. With trap cleanup EXIT

, the temp files disappear no matter which path exits.

One more point: note that ** prompt_file and output_file are global variables**. At definition time, the

cleanup

function doesn't know the contents of $prompt_file

/ $output_file

. It reads the variable values at exit, when the function runs. That's exactly why the order is: assign the variables right after mktemp

, then set the trap. Reverse the order and cleanup tries to delete empty paths and errors out.Reading the script, the task-file existence check comes before write_status "running"

.

mkdir -p "$(dirname "$handoff_file")" "$(dirname "$status_file")"

if [[ ! -r "$task_file" ]]; then
  write_status "failed" "- Error: task file is missing or unreadable (\`$task_file\`)"
  {
    echo "# Handoff"
    echo
    echo "- Failed: $(timestamp)"
    echo "- Branch: \`$(git rev-parse --abbrev-ref HEAD)\`"
    echo "- Worktree: \`$(pwd)\`"
    echo
    echo "Task file is missing or unreadable: \`$task_file\`"
  } > "$handoff_file"
  exit 1
fi

write_status "running" "- Task file: \`$task_file\`"

write_status "running"

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 "failed"

directly and exits 1.

The reason mkdir -p "$(dirname "$handoff_file")" "$(dirname "$status_file")"

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 mkdir -p dirname ...

sits at the top.

When Codex's exit code is 0, the following gets written to the handoff file.

if codex exec -p yolo -m gpt-5.4 --color never -C "$(pwd)" -o "$output_file" - < "$prompt_file"; then
  {
    echo "# Handoff"
    echo
    echo "- Completed: $(timestamp)"
    echo "- Branch: \`$(git rev-parse --abbrev-ref HEAD)\`"
    echo "- Worktree: \`$(pwd)\`"
    echo
    cat "$output_file"
    echo
    echo "## Git Status"
    echo
    git status --short
  } > "$handoff_file"
  write_status "completed" "- Handoff file: \`$handoff_file\`"

cat "$output_file"

pulls in Codex's entire output. Immediately after, echo "## Git Status"

and git status --short

follow.

That git status --short

is the linchpin of verification. Suppose Codex wrote "Files Changed: src/api/index.ts, tests/api.test.ts" in its Summary — if git status --short

shows nothing, that means git doesn't recognize any change to those files.

The cross-check commands I actually use are these.

grep -A 20 "## Git Status" /path/to/handoff-file

git diff --stat HEAD

If the handoff file's ## Git Status

and git diff --stat

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 git status --short

(= clean)." For the latter, I check the Branch

in the status file and trace it with git log

.

The reason for passing --color never to Codex lies here. If ANSI escape sequences (control characters like

\e[32m

or \033[0m

) get into output_file, they're transcribed verbatim into the handoff file. When you run grep -A 20 "## Git Status" handoff-file

, invisible control characters break the pattern match. --color never

is the instruction "don't include ANSI codes in output," and without it, mechanical post-processing gets contaminated.The handoff file on failure is minimal.

else
  {
    echo "# Handoff"
    echo
    echo "- Failed: $(timestamp)"
    echo "- Branch: \`$(git rev-parse --abbrev-ref HEAD)\`"
    echo "- Worktree: \`$(pwd)\`"
    echo
    echo "The Codex worker exited with a non-zero status."
  } > "$handoff_file"
  write_status "failed" "- Handoff file: \`$handoff_file\`"
  exit 1
fi

Just the single line The Codex worker exited with a non-zero status.

Codex's output is partially written to output_file

, but cat "$output_file"

is not executed here. Why?

The output_file

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 that when the next session reads it, you can't tell "is this completed output or partial output?" Record failure as failure, with zero content — that's the design intent.

To investigate what was happening after a failure, you need to capture the stderr of the running codex exec

separately, not output_file

. This script doesn't go that far, so on failure I enable Codex-side logging with an environment variable like CODEX_DEBUG=1

and check separately.

set -euo pipefail

When I first wrote this script, I didn't have set -euo pipefail

at the top. Bash's default behavior is to ignore errors and move to the next line.

What happened? The Codex invocation failed, but the script didn't go into the next if ... then

branch (back then it was a direct call, not an if statement) and write_status "completed"

ran. The status file said State: completed

. The handoff file had Completed: 2026-05-14T08:23:11Z

. But git diff --stat

showed nothing.

The symptom is "the status file says completed but the code hasn't changed." At first I thought "did Codex commit everything?" and looked at git log

— nothing there either.

Pinning down the cause took 30 minutes. I ran codex exec

manually and checked the exit code: it was 1. echo $?

returned 1. But inside the script, that 1 was ignored and it moved to the next line.

The fix was just adding set -euo pipefail

at the top and wrapping the Codex call in if ... then ... else ... fi

. A two-line change. But before I noticed, I'd believed a "falsely completed" status file and stacked further work on top of it, which meant redoing all of it later.

When you write a script on the premise of delegating work to an AI, ** set -euo pipefail is not an option, it's a requirement**. Bash's error-ignoring is tolerable when a human is debugging a script by hand, but with AI delegation, "silently continuing after a failure" becomes fatal.

--color never

When I tried to grep the handoff file, I got output like this.

## Git Status

?? src/^[[0mapi^[[0m/^[[32mindex.ts^[[0m

^[[0m

is the ANSI reset code and ^[[32m

specifies green. Dumping Codex's terminal output straight to a file lets ANSI escape sequences in.

In that state, running grep "index.ts" handoff-file

doesn't match, because the pattern is index.ts

but in the file it's split up as index^[[0m.ts

. It's readable to the eye, but it falls apart when you try to process it with a script.

At first I tried stripping the ANSI codes in post-processing with sed 's/\x1b\[[0-9;]*m//g'

. That works, but a sed pattern covering every terminal escape sequence is complex, and it breaks if Codex starts using different escapes in the future.

The fundamental fix is passing --color never

to codex exec

. Tell Codex not to emit ANSI codes in the first place. Controlling it at the entrance is simpler and more robust than post-processing with sed.

There was a period when I put "when you finish the work, write to the status file" in the prompt. The idea was, "if Codex itself can record completion, the script doesn't need if/else."

What actually happened: Codex wrote State: completed

. But the content wasn't real.


- State: completed
- Updated: 2026-05-20T14:33:00Z
- Branch: main
- Worktree: `/path/to/project`

Branch says main

. But the actual worktree was on the feature/api-refactor

branch. That's not git rev-parse --abbrev-ref HEAD

— it's a string Codex guessed as "probably main." The timestamp wasn't the actual completion time either; it was a plausible-looking time Codex inferred from its training data.

Worse was the case where the Codex invocation failed partway through. Codex sometimes tries to write the status file "just in case" right before exiting with an error. The result was a state where codex exec

exited non-zero, yet the status file said State: completed

.

The current prompt has an explicit prohibition.

- Do not write handoff or status files yourself; the launcher manages those artifacts.

The reason for that one line is that "if you let Codex write it, the values become guesses instead of measurements." $(git rev-parse --abbrev-ref HEAD)

is actually executed by the shell. The main

Codex writes is guessed by the model. That difference decisively changes verification accuracy. Files that record state should be written by the shell.

I once passed the task file as a relative path like ./tasks/refactor-api.md

when calling the script. At that point the behavior of mkdir -p "$(dirname "$handoff_file")"

went wrong.

Because the handoff file was also passed as a relative path like ./handoffs/refactor-api-handoff.md

, when the script changed the working directory with cd

(the version at the time did cd

into the worktree), the path dirname

computed ended up somewhere other than intended.

The symptom was a Permission denied

or No such file or directory

error meaning "can't write to the handoff file." When I debugged it and echoed the path dirname

returned, it was /handoffs

instead of /worktree/subdir/handoffs

.

The fix is to convert to absolute paths on the caller side before passing them.

bash scripts/orchestrate-codex-worker.sh \
  "$(realpath ./tasks/refactor-api.md)" \
  "$(realpath -m ./handoffs/refactor-api-handoff.md)" \
  "$(realpath -m ./status/refactor-api.status.md)"

realpath

returns the absolute path of an existing file. realpath -m

computes and returns an absolute path even if the file doesn't exist (-m

= --no-require-file

). The handoff and status files are created by the script, so they don't exist at call time. Using realpath -m

lets you fix the absolute path of a nonexistent file in advance.

Inside the script, $(dirname "$handoff_file")

then always computes the dirname of an absolute path, so it's safe to call the script from any directory.

Based on these failures, the verification flow I use now is this.

grep "State:" /path/to/status-file

git diff --stat HEAD

grep -A 10 "## Git Status" /path/to/handoff-file

Only when all three agree can I confirm "what Codex said was actually done."

Status file says State: completed

git diff --stat

shows changes → the handoff file's ## Git Status

lists the same files. That's the three-piece set.

Conversely, the patterns where it breaks down are fixed.

State: completed

but git diff --stat

is empty → Codex said "completed" without making changesgit diff --stat

shows changes but they're not in the handoff file's ## Git Status

→ there's a bug in the script's write order (I did this once)State: failed

but git diff --stat

shows changes → Codex made partial changes and exited 1 (the dangerous pattern)The last pattern needs the most caution. When Codex crashes after partial changes, git diff --stat

shows changes, but whether those changes are "partway toward a correct change" or "a broken state" isn't clear until you read the code. In that case I read the full diff with git diff HEAD

, then decide whether to fully revert or continue. Often I shelve it with git stash

before running the next Codex worker.

When the handoff file contains only The Codex worker exited with a non-zero status.

, the cause of Codex's error only exists in stderr. This script currently doesn't capture stderr, so the only option is to reproduce it. When I submit the same task next time, I add one line to the task file: "the previous run exited non-zero; identify the cause of the error before starting." I count on the root cause of the error being written in the "Remaining Risks" of the four sections.

Since I started using this "leave state in files" design, the state of "I don't know what changed" after delegating to Codex has nearly disappeared. cat

the status file, grep

the handoff file, look at git diff --stat

— with those three commands I can verify any worker's completion state in under 30 seconds. Completion that's "only said" doesn't get engraved into the filesystem.

I once handed over a task file saying "please improve the API's error handling." Codex came back in the four-section format. State: completed

was there too. The ## Git Status

section existed. But the Git Status field was empty.

Reading the handoff file's Validation section, it said "after investigation, there are no problems with the current implementation." Because the instruction was vague, "a judgment that no improvement is needed" got processed as "task complete." Doing nothing while adhering to the four-section format functions as a loophole in the design.

After I standardized the task file into the following format, this pattern nearly vanished.

対象: ~/dev/myapp/src/api/client.ts
やること: fetchUser関数のcatch節でエラーをconsole.errorに出力し、呼び出し元へrethrowする
完了条件: catch節にconsole.error + throw eが入っていること
検証: grep -n "console.error" ~/dev/myapp/src/api/client.ts && grep -n "throw e" ~/dev/myapp/src/api/client.ts

When the completion criteria and verification command are written as a pair, Codex actually runs that command in the Validation section and pastes the stdout. The escape hatch of "investigated, no problems found" is closed off.

orchestrate-codex-worker.sh passes the current worktree to Codex via -C "$(pwd)"

. What happens if you run two workers simultaneously in the same worktree?

While Codex A is rewriting src/api/client.ts

, Codex B reads the same file and makes a different change. B finishes first and lands a git commit. A, unaware of the state after B's commit, stages the file it wrote out and commits on top. The result is a commit where A overwrote lines it never intended to.

git diff --stat

had A's changes and B's changes mixed together, and I could no longer trace which worker wrote which lines. Looking at git log --oneline

, the ordering between commits was a mess too.

When running parallel workers, always separate worktrees with git worktree add

.

git worktree add ~/dev/myapp-worker-a feature/api-fix-a
git worktree add ~/dev/myapp-worker-b feature/cache-fix-b

If you move into each worktree's directory before launching the script, the status file's Worktree

field splits into ~/dev/myapp-worker-a

and ~/dev/myapp-worker-b

, and it's obvious at a glance which worker's result you're looking at.

There were times when codex exec

exited 0, yet there was nothing before ## Git Status

in the handoff file. Because cat "$output_file"

transcribed an empty file as is, Codex's four-section output was missing entirely.

The cause was a timeout on the Codex API side. When processing drags on and the API cuts the session, codex exec

can exit 0 (behavior varies by Codex version). The output_file is created but is 0 bytes.

I added two countermeasures.

codex exec

from the outside as timeout 600 codex exec ...

. It gets killed at 10 minutes and returns exit 124, so in a set -euo pipefail

environment it enters the else block and failed is recorded. Dangling workers disappear.wc -c "$handoff_file"

and, if under 200 bytes, log "the output may have been empty." The judgment is made by a human after looking at the status file.As I accumulated Codex's four-section outputs, I noticed None

lining up in the Remaining Risks section. Yet multiple cases came up where a later worker reported "there was a bug in the code the previous worker implemented." Adhering to the format while skewing the content positive — that's a common Codex tendency.

After adding the following line to the task file, concrete risks started appearing.

- Remaining Risksには必ず1件以上の懸念事項を書くこと。「None」は禁止。

You start getting things like "the type definitions are provisional," "test coverage is low," "behavior against production data is unverified." By keeping Remaining Risks at one or more items at all times, you close off one form of false completion — completion by concealing problems.

During a period when I had a folder with a Japanese name directly under my macOS home directory, I passed a handoff file path containing that path. "$(dirname "$handoff_file")"

is wrapped in double quotes so it's safe against spaces, but because the calling script omitted quotes, dirname

received a path split at the space.

The symptom is No such file or directory

from mkdir -p

. The handoff file's directory can't be created, so the task fails before it even starts.

I solved it with both thorough quoting on the caller side and realpath conversion. When passing paths containing spaces to a shell script, always wrap them in double quotes, including on the right-hand side of variable assignments.

The handoff file on failure has only the single line The Codex worker exited with a non-zero status.

Codex's error messages go to stderr, but this script doesn't redirect stderr, so they vanish the moment they hit the terminal.

The only option is to reproduce and identify the cause. I append "the previous run exited non-zero; identify the cause of the error before starting" to the next task file and hand it to Codex. I resubmit hoping the root cause of the error gets written in the "Remaining Risks" of the four sections.

Fundamentally fixing this requires a change that routes stderr to a separate file with codex exec ... 2>"$error_file"

and runs cat "$error_file"

into the handoff file on failure. It needs to be understood as an unaddressed area of the current script.

Convert the task file with realpath

(errors if the file doesn't exist) and the handoff/status files with realpath -m

(computes an absolute path even if they don't exist). Fixing relative paths into absolute paths at call time means the dirname

inside the script always computes the correct path, no matter which directory it's called from.

TASK="$(realpath ./tasks/my-task.md)"
HANDOFF="$(realpath -m ./handoffs/my-task-handoff.md)"
STATUS="$(realpath -m ./status/my-task.status.md)"
bash scripts/orchestrate-codex-worker.sh "$TASK" "$HANDOFF" "$STATUS"

A task file that ends with "please improve X" leaves the interpretation to Codex. Write the completion criteria and an executable verification command as a pair, and Codex will actually run that command in the validation section and paste the result. The escape hatch of "investigated, no problems" gets structurally closed off.

codex exec

has no CLI-level timeout argument. Putting timeout 600 codex exec ...

around it kills it at 10 minutes and returns exit 124. In a set -euo pipefail

environment it's recorded as failed. It's the minimum defense against creating workers that hang for a long time.

Don't run multiple workers in the same worktree. Separate worktrees with git worktree add <path> <branch>

and launch the script from each worktree. Because the status file's Worktree field is recorded per worktree, you can trace afterward "which worker wrote what to which branch."

When the state is State: failed

and git diff --stat

shows changes, Codex made partial changes and failed. Submitting the next worker as is means overwriting on top of the previous half-broken changes. Shelve it with git stash -u

, then append to the task file "the previous run exited non-zero. The previous changes have been shelved. Start by identifying the cause," and resubmit.

Put Remaining Risksには必ず1件以上の懸念事項を書くこと。Noneは禁止。

in the task file. A handoff file with consecutive Nones is a sign that Codex is omitting risk descriptions. With concerns continually written down, later workers and reviewers inherit the points that deserve attention.

Naming like handoff-20260824-api-fix.md

lets you trace multiple worker results chronologically. You can check the latest completion with ls -lt handoffs/

, and combining it with grep -rl "State: failed" status/

gets you a list of failed tasks. Just making filenames meaningful makes post-processing scripts far easier to write.

while true; do
  if grep -q "State: completed\|State: failed" "$STATUS_FILE"; then
    state=$(grep "State:" "$STATUS_FILE" | awk '{print $2}')
    curl -s -X POST "$SLACK_WEBHOOK" \
      -H "Content-Type: application/json" \
      -d "{\"text\": \"Worker ${state}: $(basename $STATUS_FILE)\"}"
    break
  fi
  sleep 15
done

Run Codex workers on a late-night cron and receive completion or failure over Slack. When you wake up, just reading the handoff file tells you the results of the overnight batch. With this setup I've actually received "3 tasks completed while I slept, 1 failed" two to three times a week.

This separation is condensed into the prompt's single line, Do not write handoff or status files yourself; the launcher manages those artifacts.

Break the boundary and Codex's guesses ("probably the main branch") get mixed with the shell's measurements (the result of git rev-parse --abbrev-ref HEAD

), and cross-check verification falls apart.

The cat > "$prompt_file" <<EOF ... EOF

inside the script is embedded in the script body. Every time you change a prompt rule, a diff shows up in git diff scripts/orchestrate-codex-worker.sh

. That's actually an advantage: git log scripts/orchestrate-codex-worker.sh

lets you trace the prompt's change history. Write "why I added the rule banning None in Remaining Risks" in the commit message, and months later when you read the script, the intent is clear.

The current script doesn't capture stderr. The cause of a failure vanishes the instant it hits the terminal. It's solved just by routing stderr to a separate file with codex exec ... 2>"$error_file"

and adding a line that runs cat "$error_file"

into the handoff file on failure. Making this change after the script has stabilized dramatically shortens failure-investigation time.

If you repeat the same type of work (e.g. "add validation to a new API endpoint"), templatize the task file. Create tasks/templates/add-validation.md

and swap in just the target filename with sed

before the call. Because the instructions to Codex stay consistent, the handoff file's structure stays stable too, and grep

and post-processing scripts are less likely to break.

The reasons delegation to Codex falls apart are almost always fixed. You believe the output "completed" and skip the cross-check that follows — that's it. What orchestrate-codex-worker.sh does in 108 lines is a design that structurally refuses to allow that omission.

Don't hide errors, with set -euo pipefail

. Separate the three roles of task/handoff/status files to divide the concerns. Branch name, worktree path, git status — values that need to be measured are written by the shell, not by Codex. Force four sections to demand a format from Codex's output. Prevent ANSI code contamination at the entrance with --color never

. Reclaim temp files no matter which path exits, with trap cleanup EXIT

.

Each of these design decisions is unremarkable on its own. Combined, they form a loop where you can confirm "whether what Codex said was actually done" in 30 seconds.

*Written by Lily — I ship iOS apps and automate my content stack with Claude Code.

Follow along: Portfolio · X · GitHub*

── more in #ai-agents 4 stories · sorted by recency
── more on @codex 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/make-codex-prove-it-…] indexed:0 read:25min 2026-08-30 ·