| name | claude-to-codex-session |
| description | Import Claude Code session JSONL files into local Codex sessions. Use when the user asks to convert, port, import, resume, fork-test, preserve, migrate, or validate a Claude/Claude Code transcript as a Codex session, especially for files under `.claude/projects/**.jsonl` or copied Claude session JSONL artifacts that must become `codex resume` / `codex fork` compatible. |
Claude To Codex Session
Overview
Convert a Claude Code JSONL transcript into a local Codex rollout JSONL without replaying tools. The bundled script also updates Codex discovery metadata so codex resume <id> and codex fork <id> can find the imported session.
Safety Rules
Use an explicit copied source file when the user gives one. Do not read an original Claude transcript under ~/.claude/projects/... if the user restricts the source to a copy.
Do not replay imported tool calls. Preserve them as textual response_item entries with [external_agent_tool_call: ...] and [external_agent_tool_result] markers.
Before writes, create backups of existing Codex state files. The script handles:
session_index.jsonl.bak-<timestamp>;
external_agent_session_imports.json.bak-<timestamp>;
state_5.sqlite.bak-<timestamp> when SQLite thread metadata is updated.
Keep repository files untouched unless the user explicitly asks for an artifact in the repo. This workflow normally writes only under ${CODEX_HOME:-~/.codex}.
Quick Start
If Claude Code /status gives a Session ID, use it directly from the project
directory. The script finds the matching JSONL under ~/.claude/projects and,
on a real import, copies it to ./claude-session-<id>.jsonl before converting.
Run a dry run first. Dry runs stay read-only and only report where the source
copy would be written:
claude-to-codex-session \
--claude-session-id "<claude-session-id>" \
--title "<Claude session name>" \
--cwd "$PWD" \
--dry-run \
--force-new
Run the import:
claude-to-codex-session \
--claude-session-id "<claude-session-id>" \
--title "<Claude session name>" \
--cwd "$PWD"
Use an explicit copied JSONL path when the source file was already copied or
when multiple Claude sessions match the same ID:
python ~/.codex/skills/claude-to-codex-session/scripts/convert_claude_session.py \
/path/to/claude-session.jsonl \
--cwd "$PWD" \
--dry-run
Run the import:
python ~/.codex/skills/claude-to-codex-session/scripts/convert_claude_session.py \
/path/to/claude-session.jsonl \
--cwd "$PWD" \
--expected-sha "<optional-sha256>" \
--min-tool-events 80 \
--require-away-summary
The script prints JSON with id, session_path, counts, backups, and whether an existing import was reused.
It also prints source_path, original_source_path, and content_sha256.
Validation Workflow
After import:
- Validate JSONL:
jq -c . <session_path> >/dev/null
- Verify imported markers from
response_item text:
jq -r 'select(.type=="response_item") | .payload.content[]?.text // empty' <session_path> |
awk 'BEGIN{tc=0;tr=0;as=0}
/\[external_agent_tool_call:/{tc++}
/\[external_agent_tool_result\]/{tr++}
/\[external_agent_summary\]/{as++}
END{print "tool_calls=" tc; print "tool_results=" tr; print "away_summary=" as}'
- Confirm discovery metadata:
rg '<new-id>' ~/.codex/session_index.jsonl ~/.codex/external_agent_session_imports.json
sqlite3 ~/.codex/state_5.sqlite "select id, title, rollout_path from threads where id='<new-id>';"
- Test with
fork, not resume, when the user wants proof that context survives:
codex fork <new-id> "Résume brièvement la conversation importée, puis indique le prochain pas logique sans modifier de fichier." --no-alt-screen
Use a TTY for codex fork; non-TTY runs can fail with stdin is not a terminal.
Conversion Details
The script preserves:
- user messages as
role=user / input_text;
- assistant text as
role=assistant / output_text;
- Claude
tool_use blocks as external tool-call text;
- Claude
tool_result blocks and toolUseResult payloads as external tool-result text;
system away_summary as external summary text;
- semantic Claude slash commands as compact
[external_claude_command] text;
- skill invocation context as compact
[external_agent_skill_context: ...] text;
- subagent task notifications as compact
[external_agent_subagent_result: ...] text.
The script excludes non-useful transcript noise by default:
last-prompt;
mode;
permission-mode;
bridge-session;
queue-operation;
file-history-snapshot;
attachment hook boilerplate, except subagent task notifications;
system local_command entries such as /status, /rename, and local UI stdout;
- local-command caveats, clipboard/stdout notices, and session-name reminders;
- Claude UI commands such as
/copy, /status, and /rename;
- assistant
No response requested. messages;
- empty
thinking blocks with signatures only.
The script keeps full Read, Edit, Bash, and other tool results by default.
Do not compact these unless the user explicitly asks to trade forensic detail
for a smaller imported context.
Do not import opaque thinking signatures. Import thinking only when the text field is non-empty.
Failure Handling
If codex fork <id> cannot find the session, check ~/.codex/state_5.sqlite; Codex 0.139 uses the threads table for discovery. Re-run the script with --repair-existing if the import record exists but the SQLite row is missing.
If Codex says the rollout does not start with session metadata, inspect line 1 and keep type=session_meta, payload.source=cli, payload.model_provider=openai, and payload.thread_source=user.
If the fork starts but lacks context, create a snapshot of the bad rollout, regenerate with --force-new, and validate marker counts before retesting.
If the fork modifies the imported session title in SQLite, restore a readable title with:
sqlite3 ~/.codex/state_5.sqlite \
"update threads set title='<title>', preview='<short preview>' where id='<id>';"
Script
Use scripts/convert_claude_session.py. Prefer adjusting this script over rewriting conversion logic in the chat.