The Claude-to-Codex Handoff File That Gets You a Second Developer
One file splits a session into an architect seat that decides and a builder seat that types, which is a second developer you never hired and never onboard. The claude codex handoff file is four fields and one locked row the builder cannot touch, written before the work rather than after a session dies.
>This is the contract that makes the executor swappable. Claude Fable 5 builds the audit and the plan that feed it, and the ledger that shows what each seat cost.

Hello builders,
One file splits your session into two seats, an architect that decides and a builder that types, which is a second developer you never hired and never have to onboard. When it goes wrong the model doing the typing is almost never the reason; the reason is that nobody wrote down what it needed to know, so it filled every gap by guessing. That write-down is the claude codex handoff file, and every result you will find treats it as a rescue after the session already died, whereas written first it is a job description, and the executor seat becomes a slot you can swap.
A developer described the instinct better than any spec could: I document the shit out of my tasks before a single line of code is written, and it shields me from model variability.

Four fields, one locked row, and two arrows. Instructions travel down, results travel back up, and the log at the bottom is the only part the builder cannot touch.
What has to cross the seam
A handoff is the interface between the model that decided and the model that types, and it fails at whatever it leaves undefined, because the executor fills those gaps in by guessing. Four things have to cross it.
Context, read-only ground truth. The facts the executor needs and must not change: where the code lives, what the secret is called, how the provider signs its requests. Leave a gap here and it will read the wrong files.
Task, the typing steps, lifted from the plan and concrete enough to execute without deciding anything.
Constraints, frozen boundaries. Which files are off-limits, what must never be logged, which library to use. Leave a gap here and the executor sprawls, helpfully.
Raw results, written by the builder and never edited by the architect. This is the return channel, and keeping ownership of it one-directional is what stops an executor quietly rewriting history so its own work looks better.
Then the row the picture keeps separate for a reason: a decisions log the architect owns and the builder cannot write to. Rulings live there. That separation is what makes the file authoritative instead of merely descriptive.
Here it is with a real task in it:
# Handoff: secure the payments webhook
## Context (read-only, do NOT change these)
- Repo: invoicing-api. Webhook handler: src/webhooks/payments.ts
- Secret: process.env.WEBHOOK_SECRET (already set; never hardcode it)
- Provider signs the raw body with HMAC-SHA256, header X-Signature
## Task
1. Create src/webhooks/verify.ts exporting verifySignature(rawBody, header, secret)
2. In payments.ts, call it FIRST; on failure return 401 and touch no DB
## Constraints (frozen, violating any of these fails the handoff)
- Edit ONLY src/webhooks/verify.ts and src/webhooks/payments.ts
- Never log the secret or the raw signature
- Compare with crypto.timingSafeEqual, never === (timing attack)
## Verification gate (run these, paste RAW output into Raw results)
| Gate | Command | Threshold |
|--------|----------------------------------|-----------|
| unit | npm test verify.test.ts | all pass |
| forged | curl -X POST /webhooks/payments -d @forged.json | HTTP 401 |
## Escalation triggers (STOP and hand back if:)
- The provider's scheme is not HMAC-SHA256 as assumed here
- A gate fails for a reason the task did not anticipate
- The fix seems to need a file that is not in the allowed list
## Raw results (builder writes; architect never edits)
## Decisions log (architect only)
There is a fifth thing worth adding the first week you use one, and it solves a problem you will absolutely hit: what does the executor do when it thinks the spec is wrong? Sonnet or Codex will sometimes notice, correctly, that a frozen constraint is going to cause a bug, because a cheap executor is not a stupid one. A silent compliance builds the thing the executor knew was wrong, and a silent override means it made exactly the judgment call you were routing away from it, and neither shows up anywhere until something breaks.
So give it a channel. An open disagreements section where the executor logs the conflict instead of resolving it: what the builder believes, what the spec says, the evidence, and a blank for the architect’s ruling. Now it neither obeys blindly nor freelances, and the judgment stays where it is supposed to be.
Contract on the gate, not the code
This is the line along the bottom of that picture, and it is the whole idea.
Hand the same file to two executors and they will write different code. That trips people up: if the implementations differ, how do we know they’re both right? We contracted on the gate rather than the code. The verification gate is what “correct” means for this handoff: the unit test passes, the forged request gets a 401. Any implementation that clears it is acceptable, and the exact lines do not matter.
Here is that gate as something you can actually run, judging two verify.ts implementations. One is written correctly. The other one “helpfully” trusts any signed-looking header:
WEBHOOK_SECRET=whsec_demo node -e '
const crypto = require("crypto");
const secret = process.env.WEBHOOK_SECRET;
const body = JSON.stringify({ invoice: "inv_42", status: "paid" });
const sign = (b) => crypto.createHmac("sha256", secret).update(b).digest("hex");
const correct = (b, sig) => {
const a = Buffer.from(sig), e = Buffer.from(sign(b));
return a.length === e.length && crypto.timingSafeEqual(a, e);
};
const lazy = (b, sig) => sig != null;
const good = sign(body);
const forged = good.slice(0, -1) + (good.endsWith("0") ? "1" : "0");
const gate = (v) =>
(v(body, good) ? "" : "genuine wrongly 401; ") +
(v(body, forged) ? "GATE FAIL: forged got 202" : "gate holds: forged 401");
console.log("correct verify.ts ->", gate(correct));
console.log("lazy verify.ts ->", gate(lazy));
'
correct verify.ts -> gate holds: forged 401
lazy verify.ts -> GATE FAIL: forged got 202
Same contract, same gate, and only one implementation survives it. That’s the mental shift worth making deliberately, because it’s what lets us route typing away without babysitting every character. Specifying the exact code means we did the typing ourselves and then paid a model to transcribe it, which is not really delegation, whereas specifying the outcome and letting a capable executor reach it however it reaches it is. The gate is where our trust stops: inside it the executor can work however it likes, and at that line the output gets checked against a command with a number attached.
Hand it to Codex
Now the proof. Take the exact same file and give it to two different executors:
claude --model sonnet "$(cat handoff.md)"
codex exec "$(cat handoff.md)"
Codex reads its instructions the same way Claude Code does, so the contract is portable with no reformatting. From the reference:
codex exec "summarize the repository structure and list the top 5 risky areas"
codex exec "generate release notes for the last 10 commits" | tee release-notes.md
codex exec --json "summarize the repo structure" | jq
codex exec "review the change for race conditions"
codex exec resume --last "fix the race conditions you found"
The prompt is a single positional argument, and - reads it from stdin instead. If stdin is piped and a prompt argument is given, Codex treats the prompt as the instruction and the piped content as additional context. Progress streams to stderr and only the final agent message goes to stdout, which is what makes the | tee and | jq forms above work cleanly. Flags worth knowing:
--model/-moverrides the configured model for this run.--sandbox/-stakesread-only,workspace-writeordanger-full-access.codex execruns read-only by default.--output-last-message/-owrites the final message to a file, which is a tidy way to fill the Raw results section.--output-schematakes a JSON Schema describing the expected shape of the final response.--skip-git-repo-checkallows running outside a Git repository, which Codex otherwise requires.codex reviewis its own non-interactive command, taking--base <branch>,--commit <SHA>or--uncommitted. (Non-interactive mode and Developer commands, ChatGPT Learn docs)
One correction, because it’ll save somebody a debugging session: don’t copy a pinned -m gpt-5.5 out of an older recipe. The docs no longer print that string anywhere. The model examples they do print are gpt-5.6-terra and gpt-5.6-luna. Since the whole point here is that the executor is a slot, the un-pinned codex exec form above is the right one to write down. It uses whatever your config resolves to, and it keeps working when the model names move again, which they will.
Lint the contract before you hand it over
Two of the four failure modes are purely structural, a missing section or a gate with no threshold, so a check catches them before an executor does:
node -e '
const md = require("fs").readFileSync(process.argv[1], "utf8");
const checks = [
["Context", /^##\s+Context/im],
["Task", /^##\s+Task/im],
["Constraints", /^##\s+Constraints/im],
["Escalation", /^##\s+Escalation/im],
["Gate threshold", /\|\s*Threshold\s*\|/i],
];
let gaps = 0;
for (const [name, re] of checks) {
const ok = re.test(md);
if (!ok) gaps++;
console.log((ok ? "ok " : "GAP ") + name);
}
console.log(gaps === 0
? "airtight: nothing left for the executor to guess"
: gaps + " gap(s): the executor fills each with a guess");
' handoff.md
Run it against the contract above and every gap comes back closed:
ok Context
ok Task
ok Constraints
ok Escalation
ok Gate threshold
airtight: nothing left for the executor to guess
Delete the escalation section or blank the threshold column and the matching rows flip to GAP. It won’t catch the other two failures, a vague constraint and a bloated context dump, which still need eyes. “Follow best practices” is a wish rather than a constraint, because neither we nor the executor can check any output against it. The ones that work are the ones you can falsify: edit only these two files, never log the secret, use timingSafeEqual.
So write one for a task you’re about to hand off anyway, run it on Sonnet, reset your tree, and run the identical file on Codex. If both clear the gate without you re-explaining anything in between, the contract’s real. If one asked a question the other didn’t, that question is the gap, and filling it is the entire job.
Now go build something this weekend!
John Cook