There was a time when people liked to boast about how many tokens they burned. That phase is mostly over. Tokens are not as cheap as they were, and anyone paying for their own usage has always treated the meter as a real number. I pay for Claude Pro, ChatGPT Pro and GitHub Copilot Pro out of my own pocket. So I want two things that pull against each other: get the most out of them, and not spend more than I have to. The result is a small loop where one model writes, another reviews, and a build command gets the final say.
For coding I like VS Code, but when I want an agent, my first option is Claude Code. I have used it heavily to build MedEtra in my spare time. I understand iOS in pieces, but I am not a Swift developer, so you could fairly call what I do there vibe-coding. The difference is that I vibe-code from inside a long career in IT, with firm opinions about what good code looks like even in a language I do not write fluently.
The loop I actually use
My manual loop is nothing exotic. I ask Claude Code, usually Opus, to implement a requirement. When it finishes I read the diff myself, then I hand the same change to Codex, which is usually GPT, and ask it to review. I do not always agree with what Codex finds. When a finding looks valid, I pass it back to Claude Code to check and, if it holds up, to fix. I go around a few times until I am happy. There is a human in the loop at the points that matter: the code has to compile, and I have to like the UI.
When I had a free evening I decided to automate the boring parts. I am not the first person to wire Claude Code and Codex together like this, and I could have copied or installed a solution that already works. I would rather build my own and learn how the pieces fit. I wanted to do it as a single script, for two reasons. I did not want to install yet another tool, and I wanted to watch the data move with my own eyes instead of trusting a framework to move it for me.
I built it the same way I described above. I wrote the requirement and a rough draft of the script, then asked Claude Code to implement it fully. Codex reviewed it and made a few changes of its own, partly because my Claude Code token allowance for the day had already gone up in smoke. Claude Code did another pass, I did a pass, and at some point we were all happy. So here it is: cc-codex-loop.sh.
Why it is shaped this way
In an earlier article I argued that multi-agent workflows can quietly amplify mistakes. The short version: chain enough probabilistic steps together and each agent treats the previous output as good enough. It builds on it, adds structure, sounds confident. A small wrong assumption at step two is questioned by nobody, elaborated by step three, and by step five it has become load-bearing and invisible. The pattern that survives this is not “more agents”. It is LLM, then a deterministic constraint, then LLM, then another deterministic constraint. A compiler or a test suite is a hard boundary. It can reject an output and force a correction in a way another model’s opinion cannot.
This script is that argument turned into bash. It is deliberately shallow. There is no planner, no researcher, no swarm. Two agents, one optional test command, and a hard rule that they only ever talk about one thing: the diff.
If the shape feels familiar, it should. This is pair programming, with both seats handed to models. One drives, one reads over its shoulder and objects, and they trade the keyboard until the change holds. The useful part was never the ceremony. It was the friction: two views of the same change, forced to meet in one working tree.
What the script does
The unit of work is a request file, REQUEST.md by default, where I write the task in plain English. Optionally I set VERIFY_CMD to a build or test command. That command is the deterministic anchor from the theory above, the part that is allowed to say "no" without negotiating.
The script refuses to run on a dirty tree. That sounds annoying until you see why. With a clean checkout, the git diff between the baseline commit and the working tree is exactly the change for this request and nothing else. That diff is the contract passed between the two models. They can inspect context when they need it, but the argument is anchored on the change. They argue about the diff.
The loop, in order:
- Claude Code implements the request in the working tree. It does not commit, and it ends with a short summary of what it changed.
- If
VERIFY_CMDis set, the script runs it and captures the result. - Codex reviews only the diff plus the verification result, hunting real defects rather than style. It has to end its review with one line:
PASSorISSUES. - On
ISSUES, the findings go back to Claude Code, but not as an order to comply. Claude adjudicates each one against the actual code and the original request. It fixes the findings that are real and explicitly rejects the ones that are not, ending withFIXEDorREJECTED. It keeps its session across rounds, so it still remembers what it built. - The loop stops on one of three outcomes: Codex passes, Claude rejects every finding, or it hits
MAX_ROUNDS.
That fourth step is the one I care about. The easy way to build this is to make the implementer obey the reviewer. That is also how you get two models politely talking each other into a worse answer. Letting Claude push back, and treating “the reviewer and the implementer disagree” as a reason to stop and call a human rather than a reason to loop, is the small defense against the exact failure mode I wrote about.
When it ends, a cheaper summariser writes the summary. Sonnet at low effort is the default; Haiku is there if you want to squeeze the cost further. There is no point spending Opus tokens to describe work that is already done. You get a full SUMMARY.md with the request, the per-round verdicts, the verification output, the files changed and the diff, plus a one-page version printed to the console. Everything lands under .ccx-loop/run-TIMESTAMP/. If you want Claude's raw session, it is under ~/.claude/projects/<project>/<session-id>.
A minimal run:
VERIFY_CMD="npm test" MAX_ROUNDS=3 ./cc-codex-loop.sh REQUEST.md
Questions this usually raises
Why bash? Because it runs almost anywhere with almost nothing installed, and I did not want a dependency tree standing between me and a hack. I do not particularly love bash. It loves me back about as much. It works.
Why a script and not a UI tool or a proper framework? Because I wanted to learn, and a script is the only version where I can see every prompt and every byte that crosses between the two models. If you would rather have this in Python or Go, paste the script into your favourite model and ask for a translation. That is roughly the whole point of the thing.
Why no human between rounds? For now, because I control the original request and I read the final diff anyway. The hook to put a human approval step between Codex and Claude is obvious, and I will add it the day it saves me more time than it costs. Not before.
Warnings before you run it
- You need the pieces already installed and authenticated: the Claude Code and Codex CLIs, plus
gitandjq. - It does not sandbox the commands the agents run. Run it in a repo you trust, ideally a disposable worktree or container, and avoid using it in projects with local secrets lying around. Reading the final git diff is necessary, but it will not show side effects outside the repo.
- It is a hack, not a framework. No planning step, no fan-out, no retries beyond the loop. I will improve it when an improvement pays for itself, and not for the sake of a tidier script.
- If you dislike bash, use your favourite gen-AI tool to translate it. The design is the part worth keeping, not the syntax.
The point
The lesson is that the wins do not come from adding another agent. They come from giving the few agents you have something hard to push against, and from letting them disagree out loud instead of nodding the chain toward a confident, well-formatted, wrong answer. Two models, one diff, and a build that either passes or does not. That is most of it.
Note: As I’m not a native English speaker, I used AI tools to review and refine the language of this article while keeping my original tone and ideas.
Originally published on Medium.


Top comments (0)