Your ChatGPT Work and Codex chats, inside Claude Code.
leftoff copies the chats that ChatGPT's Work mode, the Codex App and the Codex CLI keep on your computer into Claude Code's own session store. Install it once, and then:
/resumein Claude Code lists them next to your Claude chats, as⬡ ChatGPT: <title>and⬡ Codex: <title>. Open one and continue where you left off, in the folder where it ran, with the whole conversation in place.- Claude can search them. Ask "how did we fix the flaky auth tests?" and Claude looks through past chats of the project, Codex ones included, because they are ordinary Claude sessions now.
- New chats show up on their own. An async hook syncs them every time Claude Code starts. With nothing new it takes about 0.04 s and never delays startup.
In Claude Code:
/plugin marketplace add ostiums/leftoff
/plugin install leftoff@ostiums
Then run /leftoff to import the chats you already have (if the install summary asks for it, run /reload-plugins first). From then on the plugin syncs new chats every time Claude Code starts. It works the same on macOS, Linux and Windows. What the hook runs and why is explained below.
Installed it as leftoff@leftoff? The marketplace is called ostiums now: run /plugin uninstall leftoff@leftoff and /plugin marketplace remove leftoff, then the two lines above.
Installed leftoff with the curl line before? Run leftoff update after installing the plugin. The installer then removes its own hook and /leftoff, which the plugin replaces, and keeps the terminal command.
On Windows the plugin needs Git for Windows, because Claude Code runs plugin hooks in Git Bash. Without it, use the Windows installer, which does everything the plugin does.
Claude Code's built-in /import codex brings over Codex configuration (MCP servers, AGENTS.md). leftoff brings over the chats.
The plugin brings one SessionStart hook (hooks/hooks.json):
{ "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/leftoff\" sync --quiet", "async": true }bin/leftoff is a short sh script that finds Python 3.9 or newer (python3, python, then py -3 on Windows, skipping the Microsoft Store stub) and runs leftoff.py.
Without the plugin, the installer adds the same hook to ~/.claude/settings.json instead:
"hooks": {
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "/Users/you/.local/bin/leftoff sync --quiet", "async": true }] }
]
}Claude Code runs the hook every time a session starts, in any folder, including chats you open in parallel.
On Windows the entry is { "type": "command", "command": "& 'C:\\Users\\you\\.local\\bin\\leftoff.cmd' sync --quiet", "shell": "powershell", "async": true }. The shell field makes Claude Code run it in PowerShell, so it works with or without Git Bash.
Why it's needed. /resume and Claude's search through past chats only see the session files in ~/.claude/projects. Claude Code has no way to ask another tool for more chats, so a Codex chat has to be copied there before you go looking for it. Session start is the only moment Claude Code runs something on its own. Without the hook, a chat you had in Codex an hour ago stays out of /resume until you run leftoff sync or /leftoff.
What it does and doesn't do.
- Reads
~/.codex/sessionsand never changes anything there. - Writes one Claude session file per Codex chat to
~/.claude/projects/<folder>/, plus its own state in~/.local/state/leftoff/. No other files. A chat you've already continued in Claude is never overwritten. - Makes no network requests. The hook runs the copy of leftoff already on your disk and never downloads or updates it. The code changes only when you update it: in
/pluginfor the plugin, or withleftoff updatefor the installer. - Doesn't slow Claude down.
async: truemeans Claude Code starts without waiting for it, and with nothing new it finishes in about 0.04 s. - Runs once when several sessions start together. The first sync takes a lock, the others see it and exit right away, so two syncs never write the same files.
Checking it and turning it off. The hook runs leftoff.py, a single Python file that uses only the standard library, so you can read all of it. The plugin keeps its copy in ~/.claude/plugins/cache/leftoff/, the installer in ~/.local/share/leftoff. With the plugin, turn the hook off by disabling leftoff in /plugin. With the installer, leftoff autosync off removes the entry and keeps your other settings and hooks, and leftoff autosync on puts it back. To install without it, end the install command with | zsh -s -- --no-autosync, or on Windows run iex "& {$(irm https://raw.githubusercontent.com/ostiums/leftoff/main/install.ps1)} -NoAutosync". leftoff refuses to write settings.json if it can't parse the file.
All three write the same session files to ~/.codex/sessions, so leftoff reads them the same way and labels each chat with its source:
| Source | How it's detected |
|---|---|
| ChatGPT Work | ChatGPT app for Mac, Work mode (originator: codex_work_desktop) |
| Codex App | Codex desktop app |
| Codex CLI | codex in the terminal, including codex exec |
| Codex IDE | Codex extension in VS Code or JetBrains |
Requirements: Python 3.9 or newer, Codex and Claude Code, on macOS, Linux or Windows 10/11. The plugin on Windows also needs Git for Windows. The terminal installer needs git, plus zsh on macOS and Linux or PowerShell on Windows. No other dependencies. fzf is installed through Homebrew when brew is present, or through winget on Windows.
- Your messages and the Codex replies, in order, including everything before a Codex context compaction.
- Tool calls as short text blocks (
[Codex tool: exec], the command, then the output cut to 2,000 characters), so Claude knows what was run without a multi-megabyte context. - Screenshots you attached in Codex, as real images Claude can see.
- The chat title, shown in
/resumeas⬡ Codex: <title>or⬡ ChatGPT: <title>.
Left out: Codex system prompts, <environment_context> and AGENTS.md inserts, developer messages, encrypted reasoning, and the internal approval-reviewer sessions Codex Desktop creates. Codex data is only read, never modified.
Measured on 2026-09-23 with two real Codex Desktop 0.155 chats, a short one and a long one. Each converted chat was opened in Claude Code, and Claude was asked what the chat was about. The table shows the long chat: 113 tool calls, one context compaction, 43 assistant replies.
| Tool | What Claude gets | History carried | Codex system text in the history | Title in /resume |
|---|---|---|---|---|
| leftoff | native session | all 43 replies, tool calls as compact text: 189k characters | filtered out | ⬡ Codex: <title> |
| transession 0.2.0 | native session | all replies with full tool output and images: 2.6M characters, about 180k tokens on the first prompt, and one reply on Haiku cost $0.53 | kept, replayed as user messages | first message, which is Codex system text |
| codex2claude | native session (runs transession, then cleans up) | full history, 5.8 MB session file | partly filtered | first message |
| cli-continues 4.1.1 | a summary prompt in a new session | last 10 messages (50 with --preset full), and replies from before the compaction are lost in the default preset |
partly filtered | none |
| authsec-bridge | native session | replies kept, 0 of 113 tool calls | kept | none |
| resume-cli | a summary prompt in a new session | nothing: it can't read the Codex Desktop format, and Claude answered that it had no context | not applicable | not applicable |
Where the others do more: cli-continues moves sessions between 16 coding agents, and transession converts in both directions and keeps complete tool output.
Two leftoff features come from this comparison. Images you attached in Codex are passed to Claude as real images, the way transession does it. The folder a chat opens in is taken from its latest turn_context, as in PavelCz's fork of cli-continues, so a chat that moved to another folder opens where it ended.
The leftoff command in the terminal comes with the installer, the plugin doesn't add it.
leftoff # chats from the CURRENT folder: pick one in fzf and open it in Claude
leftoff global # same, across all folders (-g / --global is a synonym)
leftoff resume <id> # open a specific chat (looked up across all folders)
leftoff list [-g] [--json] # list chats: folder, title, current folder only by default
leftoff import <id> # convert only, print the command to continue
leftoff preview <id> # show the beginning of a chat
leftoff sync # import all new/changed chats so they appear in /resume
leftoff autosync on|off # run sync in the background at every Claude start<id> is a full Codex session id or any unique part of it (6+ characters).
The chat always opens in Claude in the folder where it last ran in Codex (the
latest turn_context, falling back to where it started), even if
leftoff global was started somewhere else. Folders are compared after
resolving symlinks.
In the current-folder list you see the date and the chat title. In global
mode the folder name is shown between them (~ for your home folder). When the
chats come from more than one app, a colored source column (ChatGPT Work, Codex
App, Codex CLI) appears before the title. The
preview is hidden, and Space shows and hides it. Because of that you can't type
a space in the fzf search field, so search by a single word. The full path is
shown in the preview.
Inside Claude Code: /leftoff syncs all chats. Then pick one in the
built-in /resume picker: type Codex or ChatGPT to filter (imported chats
are titled ⬡ Codex: <title> or ⬡ ChatGPT: <title>), Ctrl+A shows chats from all folders, Space previews,
Enter opens.
- Tool call input is cut to 1,000 characters and output to 2,000.
- A single message is cut to 8,000 characters. If the whole history is longer than 400,000 characters, the oldest turns are dropped and a note at the start of the chat says how many.
- Attached images (PNG, JPEG, GIF, WebP up to 5 MB) cost roughly 1 to 1.5k tokens each per request. Screenshots taken by Codex tools are not embedded, the tool output shows
[screenshot]instead. - The first message starts with a note that the chat was moved from Codex, so Claude treats the
[Codex tool: …]blocks as the Codex agent's actions. ↩Claudein the list marks chats that Codex itself once imported from Claude.- A chat that Codex imported from Claude is skipped until you continue it in Codex: until then Claude already has everything in it. This also keeps chats from bouncing between the two apps and piling up as duplicates. Once continued, a copy of a chat that leftoff brought over updates that same Claude session instead of adding a second one.
The installer adds the leftoff command to the terminal: a picker over the chats that ran in the current folder, which opens the chosen one in Claude. Without the plugin it is also a complete install on its own.
curl -fsSL https://raw.githubusercontent.com/ostiums/leftoff/main/install.sh | zshOn Windows, in PowerShell:
irm https://raw.githubusercontent.com/ostiums/leftoff/main/install.ps1 | iexIt:
- clones the repo into
~/.local/share/leftoffand links~/.local/bin/leftoff - copies the
/leftoffslash command to~/.claude/commands/ - turns on autosync: an async
SessionStarthook in~/.claude/settings.jsonrunsleftoff sync --quietat every Claude start (see The sync hook) - runs the first sync, so chats are in
/resumeright away - installs
fzfthrough Homebrew if brew is available (without fzf, chats are picked from a numbered list) - adds
~/.local/binto PATH in~/.zshrcif it isn't there yet.
Install without autosync: curl -fsSL …/install.sh | zsh -s -- --no-autosync. Switch it later with leftoff autosync on|off. Updates never turn it back on.
Update: leftoff update, or run the curl line again.
If the leftoff plugin is on, the installer only adds the terminal command: it doesn't copy /leftoff and doesn't add the hook, because the plugin has both. Moving from an installer-only setup to the plugin: install the plugin, then run leftoff update. The installer sees the plugin and removes its own hook and /leftoff, and the terminal command stays.
On Windows, install.ps1 does the same with these differences:
- the clone goes to
~\.local\share\leftoffas well, and~\.local\bingets two small launchers instead of a link:leftoff.cmdfor PowerShell and cmd, andleftofffor Git Bash - it looks for Python as
py -3,pythonorpython3and writes its full path into the launchers and the hook, so after moving to another Python, run the install line again - the hook runs in PowerShell (see The sync hook)
fzfcomes from winget, and~\.local\binis added to your user PATH instead of~/.zshrc.
The Claude session id is derived from the Codex session id, so re-importing
updates the same file. If the imported session has already been continued in
Claude (the file has grown), it is left untouched: a new session is created and
a warning is printed. sync only re-imports a chat when its Codex file has
changed since the last import, so continuing a chat in Claude never produces
duplicates by itself. State is kept in ~/.local/state/leftoff/imports.json.
leftoff was called codex-resume before. codex-resume update (or the curl line)
moves everything over: the clone goes to ~/.local/share/leftoff, the command
becomes leftoff, /codex-import becomes /leftoff, the autosync hook and the
import state are carried over. The old GitHub URL redirects to the new one.
The plugin, in Claude Code:
/plugin uninstall leftoff@ostiums
/plugin marketplace remove ostiums
Imported chats stay in /resume. The import state is in ~/.local/state/leftoff, delete it too if you're not keeping the terminal command.
The installer version:
leftoff autosync off
rm ~/.local/bin/leftoff ~/.claude/commands/leftoff.md
rm -rf ~/.local/state/leftoff ~/.local/share/leftoffOn Windows, in PowerShell:
leftoff autosync off
Remove-Item ~\.local\bin\leftoff, ~\.local\bin\leftoff.cmd, ~\.claude\commands\leftoff.md
Remove-Item -Recurse -Force ~\.local\state\leftoff, ~\.local\share\leftoffpython3 -m unittest discover -s testsMIT. See LICENSE.
