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 codex

install 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 serve

2. 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 discover
Discovered 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 openai

Import both:

agentproto auth profile import claude-code anthropic
agentproto auth profile import codex openai
agentproto 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.

slugify.js
// Turn a title into a URL slug.
export function slugify(title) {
  return title.toLowerCase().replace(/ /g, "-")
}
slugify.test.js
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")
})
package.json
{
  "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-run
agentproto 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.json

Both --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:

  1. 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.
  2. It merged that answer with what it pulled from the transcript and git and wrote the checkpoint file (context-checkpoint.ts).
  3. 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 got continuedFrom and a handoff record (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 dependencies

There 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:

slugify.js
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_a59ea793

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