Why you cannot copy a Claude Code session to another machine
You get a new laptop, copy ~/.claude across, run
claude --resume, and either nothing shows up or the conversation opens against
files that aren't there. I lost an afternoon to this before I worked out what was going on, and
the reason turns out to be much duller than the symptom suggests.
Where sessions actually live
Everything Claude Code knows about you sits under ~/.claude, and the part that
matters here is projects/:
~/.claude/
.claude.json # config, including a per-project map
projects/
-Users-alex-work-dashboard/
44ab90ee-cc33-4d22-....jsonl
8e3d21d1-1a90-4f77-....jsonl
-Users-alex-work-acme-api/
1f9c77a2-bb22-4e11-....jsonl
That folder name isn't a hash or an id, which is the first surprise. It's the absolute path
of your project with the slashes swapped for hyphens, so /Users/alex/work/dashboard
becomes -Users-alex-work-dashboard. When you run claude --resume,
Claude Code encodes whatever directory you're standing in and goes looking for a folder with
that name. Each .jsonl inside is a single session, one JSON object per line,
appended as you talk.
Three things break when you copy it
The folder name is the obvious one. Your username is different on the new machine, or the
project lives somewhere else entirely, and now -Users-alex-work-dashboard describes
a path that doesn't exist. Claude Code encodes your current directory, finds no folder matching
it, and shows you an empty history. Nothing is corrupted and nothing is lost, it's just looking
in a place you didn't put anything.
Rename the folder and you hit the second problem, which is that every line of the transcript carries the old path inside it:
{"type":"user","cwd":"/Users/alex/work/dashboard","message":{...}}
So the session appears in the list and opens, but the conversation still believes it's
running somewhere it isn't. Anything that referred to a file resolves against a directory that
went away with your old laptop. Then there's .claude.json, which keeps its own
projects map keyed by absolute path and holding per-project state, so it needs the
same treatment or the session turns up without the settings that surrounded it.
Which is really one problem wearing three hats: a session isn't portable data, it's data plus an assumption about where the filesystem is. Everything below follows from that.
Fixing it by hand
None of this is hard, it's just tedious, and if you're moving one project you may as well do
it manually. Say you're going from /Users/alex/work/dashboard on a Mac to
/home/sam/dev/dashboard on a Linux box:
# 1. copy the project folder, renamed for the new path
cp -R "~/.claude/projects/-Users-alex-work-dashboard" \
"~/.claude/projects/-home-sam-dev-dashboard"
# 2. rewrite the cwd recorded inside every transcript
cd ~/.claude/projects/-home-sam-dev-dashboard
sed -i '' 's#/Users/alex/work/dashboard#/home/sam/dev/dashboard#g' *.jsonl
# 3. re-key the project entry in .claude.json
# (open it and rename the key under "projects")
cd into the project on the new machine, run claude --resume, and
your history is back. Your login won't come across and shouldn't, since credentials are bound to
the machine that issued them, so log in once before you resume.
Why that sed line is a trap
The moment Windows is involved at either end, the replacement above silently does nothing, and it took me longer than I'd like to admit to see why. Transcripts are JSON, and JSON escapes backslashes, so the path in the file doesn't look like the path you typed:
on disk: "cwd":"C:\\Users\\alex\\work\\dashboard"
what you searched: C:\Users\alex\work\dashboard
Doubled in the file, single in your pattern, no match, no error, and a transcript that looks rewritten until you actually open it. The fix is to build both halves of the replacement by JSON encoding the paths first, which gives you exactly the escaped form the file contains. On Unix the escaped form is identical to the raw one, so the same code is correct going either direction, which is the sort of thing you only notice once it has already bitten you.
After that come the smaller ones, and there are more than you'd expect. Paths turn up inside
message bodies as well as in cwd. macOS is case insensitive and Linux isn't. A
project that lived under a temp directory on the old machine is meaningless on the new one. And
if something already exists at the destination you have to decide between merging and
overwriting, because overwriting quietly is how people lose conversations they wanted to
keep.
Codex and opencode store it differently again
If you've got more than one agent on the go, none of the above transfers, and each one fails in its own way.
Codex writes a "rollout" per conversation to
$CODEX_HOME/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl, with
$CODEX_HOME defaulting to ~/.codex. The project path lives in a
cwd field inside the file, same as Claude Code, but the layout works against you in
two ways. Sessions are bucketed by date rather than by project, so there's no folder to rename
and no way to find everything belonging to one repo without opening every file and reading it.
And some rollouts are zstd compressed, which means grep and sed skip
straight past them without complaining. If you're doing this by hand, decompress first or you'll
convince yourself the sessions aren't there.
One more thing worth knowing if you go digging: the same conversation is recorded twice in
each rollout. response_item lines are the model-facing history and
event_msg lines are a parallel stream for the UI. None of this layout is documented
upstream, so everything I know about it came from reading real rollout files against the Codex
source.
opencode skips files altogether and keeps sessions in SQLite, normally at
~/.local/share/opencode/opencode.db, with the project path in a
directory column on the sessions table. That makes the text-editing approach
useless, since there's nothing to sed, and it makes writing to the file directly a
bad idea while opencode might have it open. The safer route is opencode's own CLI, which will
run SQL for you:
opencode db "UPDATE session SET directory = '/home/sam/dev/dashboard'
WHERE directory = '/Users/alex/work/dashboard'"
Back up the database first. It's one file, so a copy costs nothing and an UPDATE without a WHERE clause is the kind of mistake you only make once.
Same conceptual problem three times over, with three unrelated storage shapes to learn and three different ways to get it wrong.
The tool I built for this
I got tired of doing the manual version and wrote entangle, a single Go binary that handles the rewriting and moves sessions between machines. Handing one to a teammate goes over magic-wormhole, so what you exchange is three words rather than a file:
you: entangle send <session-id>
-> 4-october-stormy
them: entangle receive 4-october-stormy
It lands in their project with every path rewritten for their machine and their operating
system, the transfer is encrypted end to end, nothing is uploaded anywhere and there's no
account involved, and likely secrets are masked on the way out. If you're moving your own
history to a new laptop instead, entangle export and entangle import
do the same rewriting in bulk. It reads Claude Code, Codex and opencode.
What it doesn't do
Three limits worth stating plainly, because they come up every time. Whole-machine export only reads the Claude Code layout so far, though sharing a single session works across all three tools. You can't open a Claude Code session inside Codex, and that one isn't a roadmap item so much as a thing I decided not to fake: assistant turns carry reasoning content that's opaque or signed per vendor, and inventing it gives you a transcript that reads authoritatively and misrepresents what the model actually did. And the secret masking matches credential shapes I could enumerate, which means it will miss anything with no distinctive format, which is why the bundle stays a file you can open and read before you trust it.
Source is at github.com/gowtham-sai-yadav/entangle, MIT licensed, one binary on macOS, Linux and Windows. If your team runs into this differently to how mine did, I'd like to hear about it.