Hand off a Claude Code session to Codex mid-task
Claude Code does half a task, Codex picks up the rest from a checkpoint file instead of a pasted summary. Every command replayed, rough edges included.
· Jeremy
You're halfway through a task in Claude Code and you want Codex to finish it. Maybe the context window is filling up, or you'd like the other vendor's take on the second half. The usual move is to ask Claude for a summary, paste it into Codex and hope nothing important got lost.
This tutorial does it with two commands instead. agentproto writes a checkpoint, a JSON file holding the goal, the decisions so far, the changed files, the last test run, the open risks and the next step. Then it starts a Codex session whose first prompt is that file. The Claude session is left alone, so nothing is lost if Codex goes sideways.
One thing up front: you trigger the handoff yourself. agentproto doesn't move work from one vendor to another on its own, and it doesn't swap accounts when you hit a quota. It gives you the handoff as a command.
I ran every command below on a clean setup (details at the end). The outputs are copied from that run, and the session ids are real.
What you need
- Node.js 20.9 or newer.
- Claude Code and Codex installed and logged in. You'll use your existing subscriptions; agentproto doesn't bill anything itself.
- A git repository to work in. We'll make a small one.
1. Install the CLI and both adapters
npm i -g @agentproto/cli
agentproto install claude-code
agentproto install codexinstall first fetches the adapter package, then runs its install step (the
ACP bridge each agent is driven through):
agentproto install: [bootstrap] running: npm i -g @agentproto/adapter-codex
added 17 packages in 2s
agentproto install [1/1] npm install -g @agentclientprotocol/[email protected]
added 19 packages in 8s
agentproto: 'codex' installed.Then start the daemon. That's the program that keeps running in the background
and owns your agent sessions, a bit like Docker's. agentproto serve runs it
in the foreground, which is what I did. agentproto doctor suggests
agentproto daemon install if you'd rather register it as a background
service.
agentproto serve2. Let the daemon use your logins
This step surprised me. With Claude Code logged in on my machine, the first spawn still failed:
agentproto sessions start: HTTP 500: {"error":"agent_spawn_failed","message":"agent_start: spawn failed — [missing_auth_credential at opts.auth.credential] agent-cli 'claude-code': no billing auth. Subscription → `claude setup-token`. Api-key → `agentproto auth provider set anthropic sk-…`. […] Never inherited from the shell."}That's on purpose: the daemon never picks up credentials from your shell
(define-agent-cli.ts).
You import each login once as a named auth profile. auth discover lists
what it can find:
agentproto auth discoverDiscovered credentials on this host:
✓ anthropic oauth-bearer from claude-code
Claude Code OAuth token in macOS Keychain ("Claude Code-credentials")
import: agentproto auth profile import claude-code anthropic
✓ openai oauth-bearer from codex
codex OAuth token in ~/.codex/auth.json
import: agentproto auth profile import codex openaiImport both:
agentproto auth profile import claude-code anthropic
agentproto auth profile import codex openaiagentproto auth: ✓ imported "claude-code-anthropic" (anthropic, oauth-bearer, origin claude-code)
source-backed — no stored secret
bill spawns through it: agentproto sessions start <adapter> --access-profile claude-code-anthropic
agentproto auth: ✓ imported "codex-openai" (openai, oauth-bearer, origin codex)
source-backed — no stored secret
bill spawns through it: agentproto sessions start <adapter> --access-profile codex-openai"Source-backed" means agentproto stores a pointer to the login, not a copy of
the token. On my first try the Claude token behind it had expired (401 OAuth access token has expired). Running claude once refreshed it. If you see that
error, do the same.
3. A small task to split in two
The demo is a naive slugify function plus one test. Nothing in it is
specific to agentproto.
// Turn a title into a URL slug.
export function slugify(title) {
return title.toLowerCase().replace(/ /g, "-")
}import { test } from "node:test"
import assert from "node:assert/strict"
import { slugify } from "./slugify.js"
test("lowercases and joins words", () => {
assert.equal(slugify("Hello World"), "hello-world")
}){
"name": "slugify-demo",
"type": "module",
"scripts": { "test": "node --test" }
}Commit that (git init && git add -A && git commit -m "initial slugify") so
the checkpoint has a clean git status to compare against.
4. Start Claude Code on the first half
From the repo, start a Claude Code session billed to the profile you just imported. The prompt asks for three changes but tells Claude to stop after the first one, which gives us a real mid-task state:
agentproto sessions start claude-code --label slugify \
--access-profile claude-code-anthropic \
--prompt "slugify.js is too naive. Make it (1) strip accents (é -> e), (2) drop punctuation, (3) collapse repeated dashes and trim them from both ends. Add one test per rule in slugify.test.js and keep npm test green. Do rule 1 only for now, run npm test, then stop and write down the plan for rules 2 and 3."agentproto sessions start: spawned agent-cli sess_a59ea793 (running) — npx -y @agentclientprotocol/[email protected]--access-profile is required here. Without it I got the same
missing_auth_credential error as above, even after the import.
The session runs in the background. agentproto sessions story sess_a59ea793
shows what it's doing. When it went idle, rule 1 was in place
(normalize("NFD") plus a regex that strips the accent marks), a strips accents test had been added, and npm test passed 2 of 2.
5. Preview the handoff
Before handing anything over, check what Codex would receive:
agentproto sessions handoff sess_a59ea793 --to codex --dry-runagentproto sessions handoff (dry run): sess_a59ea793 → codex
nothing written, nothing spawned. Would write: ~/.agentproto/sessions/sess_a59ea793/checkpoints/ckpt_sess_a59ea793_1791388395717.json
Approximate content: a dry run never prompts the source session, so this preview is extracted from the transcript alone (goal, decisions, tests and next step may be thinner than the real handoff). The real handoff asks the source session to summarise itself first.
[continued session — this is a structured handoff from a prior session]
Source: sess_a59ea793 · checkpoint ckpt_sess_a59ea793_1791388395717 · context was 3% full.
Original transcript: ~/.agentproto/sessions/sess_a59ea793/events.jsonl (preserved; this prompt is a summary, not a replacement).
## goal
slugify.js is too naive. Make it (1) strip accents (é -> e), …As it says, this is only an approximation. A dry run doesn't touch the Claude session, so it fills in what it can from the transcript. The real handoff does one more thing first: it asks the source session to summarize its own work.
6. Hand off
agentproto sessions handoff sess_a59ea793 --to codex \
--profile codex-openai --model gpt-5.6-sol \
--note "keep the NFD approach from rule 1; no new dependencies"agentproto sessions handoff: sess_a59ea793 (claude-code) → sess_cf278c8d (codex)
checkpoint: ~/.agentproto/sessions/sess_a59ea793/checkpoints/ckpt_sess_a59ea793_1791388396275.jsonBoth --profile and --model matter. Without them, the new session reuses the
Claude session's profile and model
(session-continue-fresh.ts),
and for a Claude-to-Codex move neither one fits. See
rough edges below for what each one does when you leave
it out.
Here's what happened in those few seconds:
- The daemon sent the Claude session a question that opens with
[handoff request from the agentproto daemon](handoff-markers.ts). Claude answered with a JSON summary of its own work. - It merged that answer with what it pulled from the transcript and git and
wrote the checkpoint file
(
context-checkpoint.ts). - It started Codex in the same directory with the checkpoint rendered as its
first prompt, then linked both sessions: the Claude session got
continuedTo, the Codex one gotcontinuedFromand ahandoffrecord (session-continue-fresh.ts).
agentproto sessions show sess_cf278c8d --json shows the link. These are the relevant fields:
"continuedFrom": "sess_a59ea793",
"checkpointId": "ckpt_sess_a59ea793_1791388396275",
"handoff": { "fromHarness": "claude-code", "toHarness": "codex", "at": "2026-10-07T15:53:25.752Z" }7. What's in the checkpoint
The file is plain JSON. Its top-level keys are schemaVersion,
checkpointId, sourceSessionId, createdAt, contextPct, policy,
sections, handoffTurn, recentDigest, originalTranscriptPath,
checkpointPath and nextAction
(checkpoint-schema.ts).
In my run handoffTurn was {"status": "answered"}, so the sections Codex
read came from Claude's own summary. Here are the ones that matter most, as
written:
== decisions ==
- Did the work inline rather than delegating to a subagent/executor, since it was small self-contained edits to two files
- Implemented rule 1 (strip accents) via title.normalize('NFD').replace(/[̀-ͯ]/g, '') before the existing toLowerCase/space-to-dash logic in slugify.js
- Added one test 'strips accents' asserting slugify('café') === 'cafe' in slugify.test.js
- Stopped after rule 1 per explicit instruction, and wrote out (but did not implement) the plan for rules 2 and 3
== risks ==
- Rules 2 (drop punctuation) and 3 (collapse/trim dashes) are not yet implemented in slugify.js, only planned
- Planned regex for punctuation (/[^a-z0-9 -]/g) and dash collapsing (/-+/g -> '-', then /^-+|-+$/g trim) have not been tested against edge cases yet
- No test added yet for punctuation or dash-collapsing rules
== nextStep ==
Implement rule 2 (drop punctuation) in slugify.js, add its test case (e.g. slugify('Hello, World!') === 'hello-world'), run npm test, then proceed to rule 3 the same way.
== notes ==
keep the NFD approach from rule 1; no new dependenciesThere are also goal, changedFiles, gitStatus, tests (the raw output
of the last npm test) and config (harness, model, profile, cwd). Your
--note goes in word for word under notes. The full Claude transcript isn't
copied: the checkpoint points at it through originalTranscriptPath and
includes a short digest of the last few turns.
8. Codex finishes the job
Codex picked it up from there. Its closing message:
Implemented rules 2 and 3:
- Drops punctuation.
- Collapses repeated dashes.
- Trims leading and trailing dashes.
- Added one focused test per rule.
Validation: `npm test` passes all 4 tests; `git diff --check` is clean.The resulting function has Claude's first step and Codex's two:
export function slugify(title) {
return title
.normalize("NFD")
.replace(/[̀-ͯ]/g, "")
.toLowerCase()
.replace(/[^a-z0-9 -]/g, "")
.replace(/ /g, "-")
.replace(/-+/g, "-")
.replace(/^-+|-+$/g, "")
}I ran npm test myself afterwards and got # pass 4, # fail 0.
Then stop the Claude session. The handoff leaves it running, in the same working tree, and two agents editing the same files is how you lose work:
agentproto sessions stop sess_a59ea793Rough edges I hit
None of these broke the handoff, but you should know about them before you rely on it.
The old profile carries over and blocks the spawn. My first attempt had no
--profile:
agentproto sessions handoff: HTTP 400: {"error":"access_profile_ineligible","message":"Failed to continue session sess_192a84f5 fresh: access_profile_ineligible — agent_start: profile \"claude-code-anthropic\" (anthropic/oauth-bearer) is not eligible for adapter \"codex\" on route \"openai\" (billed endpoint: openai)."}Refusing is the right call, since nothing gets billed to a wallet that can't
pay for it. But by then the checkpoint had already been written and the Claude
session had already been asked for its summary: persistCheckpoint runs
before the spawn
(session-continue-fresh.ts#L171).
The file just sits there unused, which is harmless. It's the next point that
causes trouble.
A retry can lose the agent's own summary. When I retried with the right
profile, Claude gave the same JSON summary as on the failed attempt, word for
word. The daemon decides whether the agent answered by checking that its last
message changed
(checkpoint-extract.ts#L388-L393).
It hadn't, so the checkpoint recorded handoffTurn: {"status": "failed", "reason": "source session produced no reply"} and left decisions and
risks empty. Codex still finished the task, working from the goal, the tests
and the next step pulled from the transcript. On the clean run above I got the
command right the first time, and every section was filled in.
Leave out --model and Codex runs its default, but the label doesn't say
so. The Claude model id was carried over, and the Codex ACP server rejected
it. The daemon log said so:
[acp] set_config_option model="claude-sonnet-5" rejected by server — keeping the agent's default model. Reason: Invalid params(acp/src/client/index.ts#L631)
Codex did the work fine on its own default model, but sessions show still
listed the session as codex · claude-sonnet-5. With --model gpt-5.6-sol
there was no rejection and sessions show printed codex · gpt-5.6-sol.
agentproto models codex lists the ids it knows about.
The source session keeps running. That's by design, so you can go back to it. It's also on you to stop it.
When to reach for this
The handoff covers the case where the session is the problem: the context is
filling up, or you want another vendor's agent to take the next step. The
checkpoint also records how full the context was (contextPct). It doesn't
help when your account hits a rate limit. That's a credential problem, and
agentproto picks the profile when a session starts, not mid-session.
Under the hood, the CLI calls the daemon's POST /sessions/:id/handoff
(http-server.ts#L6904),
and an agent connected over MCP gets the same thing through
session_checkpoint and session_continue_fresh. That would let one agent
hand work to another without you typing anything. I only tested the CLI path
for this post, so I'll leave that one for another day.
The full guide, including the agentproto sessions checkpoint verb that
writes the file without spawning anything, is
Hand off from Claude Code to Codex in the CLI docs.
How I tested this
On macOS, with @agentproto/cli 1.13.1 from npm (built from
agentproto/ts@a766f312,
which is the commit every source link above points to). To keep it clean I
used a throwaway HOME, a separate npm global prefix, a fresh
~/.agentproto, a daemon on a spare port and a new git repo. The only
shortcuts were copying my Codex login file and linking the macOS keychain so
Claude Code could find its login. Everything else is what a fresh install
gives you. In the outputs above, paths under that throwaway home are shortened
to ~.