Migration Assistant is great when one machine retires. It is the wrong tool when both keep working.

I had two Macs that both had real Claude Code history on them. The old one held tens of megabytes of project conversations going back months. The new one had a couple of hundred megabytes of more recent work, including chats from that morning. The two machines had different account names, which turns out to matter more than anything else here. I wanted the old Mac's history to appear on the new Mac, slotted into the right project folders, without touching anything the new Mac had built up on its own.

This is the merge case. Migration Assistant cannot do it. It clones a whole user, top to bottom, and the side it cloned over loses everything. So the job has to happen by hand.

Doing it by hand has its own failure mode, and it isn't the one you brace for. You expect to overwrite something and know about it immediately. What actually happens is quieter: every command succeeds, the files land, the byte counts look right, and something is still missing in a way you won't discover for weeks, because the thing that broke is not the data but the index that points at it.

Where the data actually lives

Claude Code stores all of its per-user state in two places:

  • ~/.claude/ (a directory tree)
  • ~/.claude.json (a single config file at the top of $HOME)

Inside ~/.claude/projects/, every project gets its own folder. The folder name is the project's absolute path with the slashes replaced by dashes. A project at /Users/olduser/imagine/sales-tracker lives in -Users-olduser-imagine-sales-tracker. Each .jsonl file inside that folder is one conversation transcript. There are also subfolders with hex UUIDs that hold session attachments and a memory/ directory that stores per-project memories the assistant has saved.

The folder-naming convention is what makes this migration interesting. The same project on two different Macs has two different folder names, because the username in the path differs.

What to copy and what to leave alone

The new Mac was actively in use, so the rule was: add what is missing, never overwrite what is present. That ruled out copying ~/.claude.json, settings.json, CLAUDE.md, and history.jsonl. Those are per-machine state. The old Mac's versions would have wiped the new Mac's working setup.

What was safe to merge:

  • projects/ (each conversation is a standalone file, additive by nature)
  • plans/, sessions/, file-history/, shell-snapshots/

I took a snapshot of ~/.claude on the new Mac first, into ~/.claude.bak-$(date +%F). That single line was the entire safety net for the rest of the work.

What I Assumed About an Additive Copy

That a copy which only ever adds files cannot lose anything.

rsync --ignore-existing is about as safe as a merge gets. It writes files that are missing and refuses to touch files that are present. There is no overwrite, so there is nothing to lose. I ran it, it reported success, and it was right about every file it touched.

It still cost me data, and it took time to notice, because nothing was deleted and nothing errored. The problem is that "never overwrite what exists" is the correct rule for content and exactly the wrong rule for anything that indexes content. If both machines have their own version of an index, an additive copy keeps one of them and quietly orphans everything the other one was pointing at. The files are all present. They are simply invisible.

That distinction, between files that are their own record and files that are a record of other files, decided every real decision in this migration. The renamed-project section below is where it actually bit.

The username problem

Old Mac project folders all started with -Users-olduser-. New Mac project folders all started with -Users-newuser-. If I copied the folders verbatim, Claude Code on the new Mac would never find them when working in the matching project, because the cwd-to-folder lookup hashes against newuser, not olduser.

So the copy step also did a rename. For each -Users-olduser-X folder, I rsynced its contents into -Users-newuser-X on the destination. Where a folder of that name already existed (because the same project lived on both Macs), the .jsonl files merged in alongside the new Mac's existing ones. Each conversation stays whole; nothing collides because the filenames are UUIDs.

A small gotcha: folder names that start with a dash break almost every command-line tool. ls -Users-olduser-foo thinks -Users-olduser-foo is a flag. Prefixing with ./ solves it.

The renamed-project case

One project on the old Mac was called notes-api-master. On the new Mac the same logical project had been renamed to fieldnotes. The conversations from the old name needed to land in the folder for the new name.

This is the same pattern as the username rename, just one level deeper. I rsynced the contents of -Users-olduser-imagine-notes-api-master/ into -Users-newuser-imagine-fieldnotes/. Worked cleanly for the conversation files. Worked partially for the memory/ subfolder.

Memory files come in two shapes. There are individual .md files (one per remembered fact), and there is a MEMORY.md index file that lists them all. The individual files merged cleanly because rsync is happy to add new files alongside existing ones with --ignore-existing. The index did not, because both sides had their own version of MEMORY.md. Rsync kept whichever was already there, which meant the destination index referenced one entry while twelve files sat unindexed on disk next to it.

The fix was to combine the two index files by hand: keep the destination's existing entry, append the source's entries below it, and write the result back. The individual memory files were already in place from the earlier rsync, so they immediately became discoverable again.

The code repos

The Claude history was the headline migration, but the same machine had a ~/imagine directory with a few dozen project folders missing on the new Mac. I took the diff (folders present on old, absent on new) and rsynced them across, with a standard exclude list: node_modules, .next, dist, build, out, target, .venv, venv, __pycache__, .gradle, vendor (for Composer), .cache, .turbo, coverage, .parcel-cache, .idea, .DS_Store. A couple of very large folders (multi-gigabyte assets) got skipped by name to keep the transfer fast.

The whole code transfer ran in a few minutes over the LAN once the excludes were right. Without them, a handful of folders alone would have been several gigabytes of node_modules that npm can rebuild from package.json in seconds.

Takeaway

Sort your files into two piles before you copy anything: the ones that are their own record, and the ones that are a record of other files. Only the first pile is safe to merge automatically.

That is the whole lesson, and it long outlives Claude Code. Conversations, notes and photos are self-contained, so an additive copy handles them perfectly. Indexes, manifests, lockfiles, playlists, .gitmodules, anything that names other things: those cannot be merged by a tool that only knows how to skip what already exists. It will keep one side's index, orphan the other side's contents, and report success. The data survives. The pointer to it doesn't.

Where this doesn't apply: if the old machine is genuinely retiring and has nothing on the new one worth keeping, use Migration Assistant. It's faster, it's supported, and hand-rolling rsync for a one-way move is inventing risk for no reason. All of this only earns its keep when both sides have state you want, which is the point a move stops being a transfer and becomes a sequence of decisions.

The bit I'd still like a better answer for is detection. I found the orphaned memory files by opening a project and noticing something I knew I'd written was missing. That is not a method. The honest version of this ends with a script that walks each index, asserts every file it names exists, and asserts every file on disk is named by some index. I have not written it yet.


PS: what already exists, which I only checked afterwards

I did this by hand and wrote it up before checking whether anyone had solved it. They partly had. Anthropic documents the directory itself now, too, which it didn't when I started: Explore the .claude directory.

Tool Solves Two active machines?
claude-code-migrate One-way move to a new machine, including username translation No, refuses if the destination already exists
claudepath Renaming or relocating a project, remapping every path reference Partly, --merge combines session directories
claude-mv Moving a directory and its Claude context together No, single machine
claude-code-sync Ongoing sync across devices via a git remote Yes for conversations, with the caveat below

One caveat is worth more than the rest of that table. claude-code-sync is the closest thing to continuous sync, and it requires that "your projects must be at the same absolute paths on all machines". The username is part of that path. So on two Macs with different account names, every project resolves to a different slug and the sync has nothing to match. That is this same wall, arriving from the other side, and no search that describes the symptom will surface it. The encoding is lossy in other ways too: non-ASCII paths collide outright.

So the path half of this is solved several times over and I should have looked first. The index half isn't. All of these move transcripts and rewrite paths; none of them reconciles a MEMORY.md that both machines have their own copy of. That check is still yours to run, whichever tool you use.