The one-word test that told me my delegate model didn't exist

  • AGENTS

The one-word test that told me my delegate model didn't exist

I told Claude Code to hand bounded coding work to Codex. A wrong path exited 127 and read as \"unavailable,\" a missing redirect hung forever, and the CPU meter could not tell a stall from a healthy run.

My global Claude Code instructions have a section that splits the work in two. Claude is the architect. It inspects the code, makes the design calls, breaks the task into pieces, reviews the result and runs the verification. Anything bounded and checkable goes to a headless Codex run on gpt-6-luna at max reasoning effort, which I treat as a capable engineer rather than a typist. Spelled-out diffs, renames, a feature from a concrete spec, a bug with a known repro, tests for behaviour that already exists. Open design questions, security-sensitive changes and anything that deletes data stay with Claude, and so does the final claim that the thing works.

The point is context. Claude's window should hold decisions, not the fourth file of a pattern migration. It's a standing rule, so I never have to ask.

A standing rule is only as good as the one line in it that names a binary. I found that out the slow way.

Exit 127 means nothing, loudly

On September 16 the rule pointed at /opt/homebrew/bin/codex. There is no such file. It looked right, which is the problem with paths.

Here is what that path does today:

$ /opt/homebrew/bin/codex --version; echo "exit=$?"
no such file or directory: /opt/homebrew/bin/codex
exit=127

127 is the shell saying "command not found." It is not Codex saying anything, because Codex never ran. But from inside an agent's reasoning, a failed dispatch with a non-zero exit reads very naturally as "Codex is unavailable right now," and that is how it was read. One delegation was lost to it. Nothing crashed. The work that should have gone out simply didn't.

Part of why it looked right: codex works fine in my terminal. In my interactive shell it's a zsh function defined in ~/.zshrc. Claude's Bash tool doesn't load that, so the function doesn't exist there and neither does the name on its PATH. "It works when I type it" proved exactly nothing about the environment that mattered.

The fix to the rule was a sentence: always call an absolute path, and treat 127 as a wrong path, never as an outage.

Two binaries, one model

The second trap is which binary. There are two Codex CLIs on this Mac.

$ ~/.local/bin/codex --version
codex-cli 0.154.0
$ /Applications/ChatGPT.app/Contents/Resources/codex --version
codex-cli 0.155.0-alpha.9.2

The first is the stable release from npm. The second is a 234 MB binary tucked inside the ChatGPT desktop app. They are not interchangeable. Ask the stable one for the delegate model and it refuses with an HTTP 400:

The 'gpt-6-luna' model is not supported when using Codex with a ChatGPT account

As far as that CLI knows, my delegate model doesn't exist. codex update doesn't help, because 0.154.0 is already the newest version on npm. Only the build bundled in the app can reach it.

This is why the rule now records how it was verified, not just what to run. On September 23 I confirmed both behaviours with a live one-word dispatch. One word is the cheapest possible prompt that still proves the whole chain: this binary, this model, this account, a real answer coming back. A --version check proves the file exists. It says nothing about whether the model on the other end will talk to you.

The redirect that looks optional

The third trap needs no wrong path at all. The run starts, and then does nothing, forever.

When Codex runs headless it inherits the caller's stdin. If that stdin is still attached, it prints Reading additional input from stdin... and waits. It never starts the task. The process is alive, the log barely grows, and there is no timeout coming.

The sneaky part is how a brief naturally gets written. The obvious move is a heredoc that writes the brief file and then dispatches, all in one Bash call. That one call is exactly what leaves stdin attached. So the rule now says two things: < /dev/null on every dispatch, and write the brief in a separate call from the one that sends it.

The command it settled on:

/Applications/ChatGPT.app/Contents/Resources/codex exec -m gpt-6-luna \
  -c model_reasoning_effort=max \
  -C <worktree> -s workspace-write --skip-git-repo-check \
  -o <scratchpad>/luna-last.md "$(cat <scratchpad>/brief.md)" \
  < /dev/null > <scratchpad>/luna-run.log 2>&1

Approvals are already set to never in the Codex config, so the run stays non-interactive. The final message lands in one file and everything else in the log, and Claude reads the final message plus targeted slices of the log, never the whole thing.

A live process is not progress

The stdin hang is also a lesson in how to check on a delegate. The obvious check is ps. The process is there, so it must be working. It isn't.

The next obvious check is CPU. That's worse. A Codex run spends most of its life waiting on a model call over the network, and during every one of those calls %CPU reads 0.0. A healthy run and a stalled run look identical on that column.

What actually separates them is the log. A run that has really started gets past a few kilobytes, and a run stuck on stdin sits at tens of bytes no matter how long you wait. So the check is wc -c on the log file. If it's still tiny after the run should have started, it's a stall, and the rule says to report it as a stall instead of as "still working."

Every one of these failures leaned toward a comfortable answer. Exit 127 said the tool was down, not that my path was wrong. The hang looked like a busy process. The stable CLI looked like Codex until it was asked for the one model I cared about. One real word, sent through the real command, catches all three, and it should have been the first thing I ran.