Importing Claude Code memory into Codex 0.165: enable memories before the first import, or the profile stays on v1
Copying the notes is the easy part. Every Claude Code file survives intact, and what Codex loads at session start is a model-written route to it, inside a summary with a hard size cap. Two default-off switches and one stray directory decide which memory pipeline those files land in.
Contents
The Codex CLI 0.165.0 release notes, out on October 27, give the change one line: memory v2 is now the default for fresh profiles, and Codex can import “external-agent memories” into it. External agent here means Claude Code. The importer reads the auto memory Claude Code keeps per project, copies it into the Codex memory workspace, and queues a model pass that folds it into the summary Codex injects into every new session.
The line leaves out the flag and the order. The import sits behind a feature flag that is off and marked as under development, so on a stock install /import does not offer memory at all. And whether you end up on v2 depends on the configuration the first import runs under: with only the import flag on, the import itself creates the directory that keeps your profile on v1.
I read the source at the rust-v0.165.0 tag and then ran the import through codex app-server against a throwaway home directory with three fake Claude Code projects. Everything below comes from one of those two, and I say which.
Can Codex 0.165 import Claude Code memory out of the box?
No. The memory row of the import flow appears only when the external_agent_memory_import feature is on, and in features/src/lib.rs it is Stage::UnderDevelopment with default_enabled: false. Memory itself is a separate feature, memories, which is stable but also off by default.
In the run, detection on a clean profile returned only the recent chat sessions. After adding the flag to config.toml, a Memory item appeared. Set both features together, before any import:
[features]
memories = true
external_agent_memory_import = trueIf you send an import request for memory without the flag, the app server refuses it with external agent memory import is disabled. In the TUI, /import puts Memory in the home section as one checkbox, ticked by default along with everything else it detected.
Which memory version does Codex use after importing Claude Code memory?
When memories.version is unset, Codex defaults to v2 only if the memories feature is on, memories.dual_write is not explicitly false, and ~/.codex/memories, the v1 directory, does not exist. That is the whole of “fresh profiles default to v2” in core/src/config/mod.rs. Otherwise it falls back to v1. An explicit memories.version always wins.
The importer has its own rule: it always writes ~/.codex/memories_v2, and it also writes ~/.codex/memories when the resolved version is v1 or that directory already exists. The sequence most people will try goes wrong. You turn on only the import flag, because that is the one the import needs. With memories off, the version resolves to v1, the import writes both namespaces, and ~/.codex/memories now exists. When you later turn memories on, Codex sees a v1 workspace and keeps you on v1.
That is what the run showed. With only the import flag set, the import created both memories and memories_v2. On a second clean profile with memories = true in the same file, it created memories_v2 alone. The app-server integration test encodes the same matrix, five cases from “new v2 user” to “legacy workspace”.
One setting reads the other way from its name. memories.dual_write = false does not stop the import writing two copies. It is the opt-out from the v2 default, and keeps a fresh profile on v1. Real dual-write, where startup runs both pipelines, happens only with version = "v1" and dual_write = true, and dual_write defaults to false.
Setting version = "v2" under [memories] pins the startup pipeline to v2, but it does not enable memory, and it does not stop the import from also writing to ~/.codex/memories if that directory exists. If you already have one, the import keeps both copies current.
What does the Codex importer copy from Claude Code’s memory folder?
Every Markdown file under each project’s memory/ directory, byte for byte. Claude Code keeps auto memory in ~/.: a MEMORY.md index plus one topic file per memory, with frontmatter. The importer walks that tree recursively, takes regular files ending in .md in any letter case, and skips symbolic links, both linked files and linked directories.
Each project lands in its own folder with one extra file:
~/.codex/memories_v2/extensions/external_agent_import/
├── instructions.md
└── resources/
└── -work-app-a/
├── scope.json {"cwd":"/home/you/work/app-a"}
├── MEMORY.md
├── build.md
└── notes/deep.mdIn the run, cmp found the copied topic file identical to the source, frontmatter included, and the nested notes/deep.md kept its path. A symlinked linked.md in the same folder did not arrive. The folder name is the Claude Code project key, the escaped path you see under ~/.claude/projects. Your MEMORY.md is copied as it is, and the consolidation instructions forbid editing anything under resources/. Nothing is written back to Claude Code.
Which Claude Code projects does the Codex importer skip?
Any project for which it cannot find a working directory. Claude Code’s memory folder does not record which directory it belongs to, so the importer reads the project’s session transcripts, newest first, and takes the cwd of the first usable one. Usable means the transcript has a conversation message with a timestamp, was not replaced by a continuation, and names an absolute directory that still exists. That becomes scope.json. A project with no usable session is not offered for a first import.
Two ordinary situations produce that. Claude Code deletes old session transcripts after its cleanupPeriodDays retention period, but keeps the memory directory, so a project you have not opened in a while can have memory and no transcripts. And a project whose directory you moved or deleted has transcripts pointing at nothing. My fixture had one of each. Neither was listed, and when I added them to the request by hand, the result reported both as failures:
selected memory was not found: -work-app-b
selected memory was not found: -work-app-cA project that was imported earlier and has since lost its transcripts keeps its existing copy; the importer only skips refreshing it.
The source is normally $HOME/.claude. The importer does not read Claude Code’s own location overrides, so if you moved auto memory with autoMemoryDirectory, or run Claude Code under CLAUDE_CONFIG_DIR, it does not look there. Per the Claude Code docs, all worktrees of a repository share one memory directory. The importer scopes it to whichever checkout the newest usable session ran in, which may be a worktree rather than the main repository.
This script is a heuristic for the cwd half of that logic, so you can see which projects have a usable directory before you start. The authoritative list is what detection offers; the script does not compare against copies that already exist.
import json, os, pathlib
def usable_cwd(session):
cwd, has_message = None, False
try:
lines = session.read_text(errors="ignore").splitlines()
except OSError:
return None
for line in lines:
try:
rec = json.loads(line.strip())
except ValueError:
continue
if not isinstance(rec, dict):
continue
if rec.get("type") == "continued-in" and rec.get("continuedInSessionId"):
return None
if cwd is None and isinstance(rec.get("cwd"), str):
cwd = rec["cwd"]
if isinstance(rec.get("message"), dict) and rec.get("timestamp"):
has_message = True
if cwd and has_message and os.path.isabs(cwd) and os.path.isdir(cwd):
return os.path.realpath(cwd)
return None
root = pathlib.Path.home() / ".claude" / "projects"
for proj in sorted(root.iterdir()) if root.is_dir() else []:
mem = proj / "memory"
if proj.is_symlink() or mem.is_symlink() or not mem.is_dir():
continue
files = [f for f in mem.rglob("*") if f.suffix.lower() == ".md"
and f.is_file() and not f.is_symlink()]
sessions = [s for s in proj.glob("*.jsonl") if s.is_file() and not s.is_symlink()]
sessions.sort(key=os.path.getmtime, reverse=True)
cwd = next((c for s in sessions if (c := usable_cwd(s))), None)
print("CWD FOUND" if cwd and files else "NO CWD ", len(files), proj.name, cwd or "")On the fixture it printed one CWD FOUND and two NO CWD lines, matching what detection offered.
What does Codex consolidation do with your Claude Code MEMORY.md?
It reads it and writes routes to it into a separate file. After copying, the importer queues a consolidation job, a model pass that writes memory_summary.md. The v2 consolidation prompt says that file “will be injected at the beginning of every new session”, and the validator rejects a summary that does not start with the line v1, is 10,000 bytes or more, or lacks any of these headings:
## User Profile
## User preferences
## General Tips
## What's in MemoryThe importer adds its own rules in instructions.md. These four bullets, quoted whole, decide what happens to a project you curated by hand:
- For each project, always read its source `MEMORY.md` first when it exists.
Use it to add or update the project's memory entry in the active pipeline's
output files.
- Treat every other source `*.md` file as detailed supporting evidence analogous
to a rollout summary. Do not flatten its full contents into Codex `MEMORY.md`
or `memory_summary.md`. Keep the detail in the imported resource, add a concise
pointer in `memory_summary.md` when useful, and read the resource progressively
when a later task needs that topic.
- In `memory_summary.md`, represent imported project memory only as a compact
route under `## What's in Memory`. Do not copy its contents into
`## User Profile`, `## User preferences`, or `## General Tips`, even with a
project-scope qualifier.
- Imported resources have no rollout `updated_at`. When no reliable source date
exists, route them under `### Older Memory Topics`; do not invent a date or use
the consolidation date.So the instructions ask the model to turn each Claude Code project into a compact route, when it judges one useful, and to file it under Older Memory Topics unless it finds a date it trusts. All of that competes for one 10,000-byte summary with every other imported project and with Codex’s own memory. A preference you wrote down once in Claude Code does not become a Codex preference, because the rules forbid lifting imported content into the profile sections. Whether the model treats the modified timestamp Claude Code writes into frontmatter as a reliable date is left to the model. Nothing in the code parses it.
The rule is defensible: a preference recorded for one repository should not turn global because it was copied. It does mean the import is not a migration of how you work. It is a library of project notes with a catalogue entry for some of them, and the entries are written by a model. I made a related argument in Agent memory as Markdown documents: the index is the retrieval layer. Here your index survives on disk, and the one Codex actually loads is somebody else’s summary of it.
Consolidation runs in the memory startup pipeline, which returns early when the memories feature is off. With only the import flag on, the files arrive and nothing reads them. I did not run consolidation, so I cannot show what the routes look like. That part is the prompt and the validator, not an observation.
What happens when you re-run the Codex memory import?
It synchronizes the projects you select. For each one, the importer deletes the project’s folder under resources/ and writes it again from the source, so a file you removed in Claude Code disappears from Codex too. In the run, deleting build.md from the fake project and importing again removed it from both namespaces. Detection also looks at the copies Codex already holds: a copied project that no longer exists in Claude Code is offered again, and importing it deletes the copy. A project whose copy is already identical is not offered at all.
The sync covers only the copies. What Codex learns in its own sessions lives apart, in rollout summaries, and is never written back to Claude Code. Each import queues a new consolidation, which applies the source changes to the shared summary and drops claims that only a deleted file supported. The two memories of one project still drift apart from the first Codex session onward. If you run both tools on the same repository (the splits people use), decide which tool’s memory is the record before you schedule re-imports.
Why does a Codex memory import fail with “memory workspace is busy”?
Because a consolidation or another import holds the workspace lock. Each namespace has a lock file next to it, ~/. and ~/.codex/memories.lock, and the import takes every lock it needs before it changes anything. If one is held, it stops with:
memory workspace is busy; retry after consolidation or another import finishesNothing has been written at that point, so there is no half-imported v1 copy to clean up. Retry once the consolidation finishes. Pull request #56462 closed the race between import and consolidation.
What to check before importing Claude Code memory into Codex
Try it on a scratch profile first. Point CODEX_HOME at an empty directory, put both features in that directory’s config.toml, and start codex from a disposable working directory, because other import items can add files to the current project. In /import, choose Customize selection and leave only Memory ticked. Then look at what lands in memories_v2/ before you do the same on your real profile. The import reads ~/.claude and does not modify it.
On the real profile, check three things:
- Put
memories = trueand the import flag inconfig.tomlbefore the first import. If~/.codex/memoriesalready exists, automatic v2 is off the table; setmemories.version = "v2"if you want the v2 pipeline anyway. - Run the script above. Every
NO CWDline is a project whose memory will not come across, usually because its transcripts were cleaned up. Starting a conversation in that project with Claude Code writes a new transcript with a livecwd. - Read the full project list before you approve it. The TUI shows the count and only the first four project keys, and its one checkbox imports every detected project into one profile and one summary.
scope.jsonplus a sentence in the instructions are all that keep one project’s notes out of another’s sessions. If some of those projects belong to other people, or simply should not share a summary, use a separateCODEX_HOMEfor them, or the app-server API, which takes an explicit list of project keys.
In this import, the defaults are two off switches and one directory that should not exist yet.
01 / Discussion
No comment section here — all discussions happen on X.
02 / More articles
GPT-6.1 Terra for computer use: a worker seat in the agent stack, not the reviewer's
A mid-tier model earns its price where the result is cheap to check before it matters. Terra's benchmarks measure cost per task; the number that decides computer use is what a wrong click costs, and OpenAI's chart has no axis for it.
OpenAI's no-internet rule for frontier training: the approved services were the way out, and agent sandboxes should copy the split
The copy of the web held. What OpenAI's own red team found a way through was almost every service left connected for running the lab, and the September escape went through a resolver no copy replaced. Serve the agent's work from copies, keep the live connections few and outside its reach, and expect the cost to land on github.com.
Vox's AGENTS.md lines, tested in 90 Claude Code runs: the line that worked best wasn't one of his
Line one made the agent leave out the input check. Line two's extras disappeared when line one shared the file. 'Without breaking anything' showed no clear effect beyond what a named test command already did. What 90 runs can and cannot say, and how to test a line before you keep it.