[FEATURE] `claude project mv` — one command to re-point a project's state after its directory moves (transcripts, memory, prompt history, session registry, profiles)
Preflight Checklist
- [x] I have searched existing requests. Related issues exist, but none of the open ones asks for this specific thing — see "Why this is not a duplicate".
- [x] This is a single feature request
Problem Statement
Moving a project directory (mv ~/Projects/foo ~/Projects/client/foo) silently orphans more than session transcripts, and there is no built-in way to re-point any of it. The path slug under ~/.claude/projects/ is the primary key, but it is not the only place the absolute path is stored.
I moved one project today and had to migrate all of this by hand (Claude Code 2.1.278, Linux/WSL2):
| state | location | what breaks |
|---|---|---|
| session transcripts (92 MB, 42 sessions) | ~/.claude/projects/<slug>/*.jsonl + sidecar dirs (<session-id>/tool-results/*.txt) | /resume, --continue show an empty project |
| project memory | ~/.claude/projects/<slug>/memory/*.md + MEMORY.md | silently gone — the model starts the next session with no recall of the project, and nothing warns that a populated memory dir exists one slug over |
| prompt history (695 entries) | ~/.claude/history.jsonl, {"project": "<abs path>"} per entry | up-arrow history for the project is empty |
| project entry (trust, allowedTools, MCP servers, stats) | ~/.claude.json → projects["<abs path>"] | trust dialog and per-project permissions start from scratch |
| session registry | ~/.claude/sessions/<pid>.json (6 files) | stale cwd; see #83146 for what that then breaks |
| background job state | ~/.claude/jobs/<id>/state.json | stale paths |
| every one of the above, again, per profile | ~/.claude/profiles/<name>/... | a second, separately-keyed copy is orphaned the same way |
Nothing in the product detects this. The old slug directory simply stays behind, the new one is created empty, and the only signal is that the model has amnesia about a project it worked on yesterday.
Proposed Solution
A first-class migration command, e.g.
claude project mv <old-path> <new-path> # or: claude project relink --to <new-path>
that moves/merges every item in the table above, including profiles, and rewrites the recorded absolute path inside transcripts.
Two cheap additions that would help even without the command:
- Detect it. On startup in an empty project, if a slug directory exists for a path that no longer resolves and the current directory looks like the same project (same git remote + same basename, or same git
.gitinode), print a one-line hint:Found 42 sessions and 11 memory files under a previous path <old>. Run \claude project mv\to bring them over. - Warn on orphans.
claude doctorcould list slug directories whose path no longer exists.
Why this is not a duplicate
- #28745 (80 👍) was closed with "this works now —
claude --resume <session-id>from any directory". That solves resuming one known session id. It does not restore project memory, prompt history, the~/.claude.jsonproject entry, or the/resumepicker (still broken per the last comments there, and per #92160). - #5768 (60 👍, open) is the resume-by-path bug, not a migration story.
- #83146 and #86713 (open) describe consequences of the stale/duplicated path — the registry never revalidating
cwd, and the picker keyed only to the old slug. Neither asks for a migration path for data that is already orphaned. - #96013 (open) proposes project-local session storage. That is a larger design change and, notably, still would not cover
~/.claude.json, the session registry, or profiles. A migration command is useful either way, and is needed for every project that exists today. - The earlier requests for exactly this command — #70470 (
claude history migrate <old> <new>) and #24789 (claude project migrate --from --to) — were auto-closed as duplicates by the triage bot and are now locked, which is why this is filed fresh, as the lock messages instruct.
Implementation notes from doing it by hand
Things a naive sed migration gets wrong, and that an official implementation should handle:
- Prefix collisions. My project was
~/Projects/belfermer; a different project~/Projects/belfermer-dockerexists next to it. A plain string replace of the old path corrupts the sibling. The replace needs a boundary check ((?![-\w])or equivalent), and the same applies to the slug encoding, where/and_both map to-and are not injective (see #80264, #21085). - Live processes.
~/.claude.jsonis rewritten wholesale from an in-memory copy when any session exits, so an edit made while other sessions run can be silently reverted. The command should refuse to run, or warn, while other sessions are alive. - Sidecar data. Session subdirectories (
tool-results/*.txt) contain absolute paths too and are easy to miss — a transcript-only migration leaves them stale. - Profiles.
~/.claude/profiles/<name>/duplicates the whole layout; migrating only the active profile leaves the other silently broken.
Priority
Medium — Would be very helpful
Feature Category
Other (CLI / project state)
Additional Context
Third-party tools exist for this (several claude-mv variants, FarisHijazi/claude-migrate as a plugin), which is itself a signal — but the ones I looked at migrate the transcripts only, and not memory, ~/.claude.json, prompt history, the session registry or profiles. Users can't reasonably be expected to discover that list empirically.
3 Comments
Prior art: what the existing third-party tools do, and where each stops
Since "third-party tools exist" is easy to read as "so the gap is filled", here is what they actually cover. I read the source of each rather than the README, because the two diverge in places. Stars and dates as of 2026-09-22.
| | claude-migrate ⭐6 | kriste11er/claude-mv ⭐2 | dbucovsky/claude-mv | deviationist/claude-mv | NotebookNomad/claude-move |
|---|---|---|---|---|---|
| form | plugin (
/migrate) +uvx| bash, 36 KB | python, 11 KB | python, 129 KB | python, 164 KB || transcripts | ✅
copytree| ✅ | ✅ | ✅ | ✅ ||
memory/| ✅ (rides along in the slug dir) | ✅ + interactive handling on session reassign | ✅ | ✅ | ✅ explicitly ||
~/.claude.jsonproject entry | ❌ | ❌ | ❌ | ✅ | ✅ ||
history.jsonl(prompt history) | ❌ | ✅ | ✅ | ✅ | ✅ ||
sessions/<pid>.jsonregistry | ❌ | ❌ | ❌ | read-only, for the liveness check | ✅ via generic sweep ||
file-history/,todos/,shell-snapshots/,debug/| ❌ | ✅ | ✅ | n/a by design (session-keyed) | ✅, incl. renamingfile-historyblobs whose names aresha256(path)||
jobs/,plans/, other top-level state | ❌ | ❌ | ❌ | n/a by design | ✅ generic sweep of everything butprojects/,file-history/, its own backups || profiles /
CLAUDE_CONFIG_DIR| partial | ❌ | ❌ | ✅ first-class | ❌ || replacement is boundary-safe | ✅ documented | ❌
sed "s\|$old\|$new\|g"| ❌text.replace(old, new)| ✅ by construction — rewrites thecwdJSON field only | ✅ explicit(?![A-Za-z0-9_-])(?!\.[A-Za-z0-9_-])|| refuses to run with live sessions | ❌ | partial | ❌ | ✅
check_live_sessions()| ✅ || backups | ❌ (copies, doesn't move) | ✅ tarball | ✅ per-file | ✅ restore root | ✅ |
Two of these are genuinely good —
deviationist/claude-mvis the only one that understands profiles, andNotebookNomad/claude-moveis the only one that renamesfile-historyblobs and sweeps the remaining top-level state. Neither is a superset of the other, so even a well-informed user has to run two tools, or accept a gap.The pattern across the table is the actual argument for doing this in the product:
sed,str.replace) are exactly the sibling-prefix bug from the issue body — with~/Projects/fooand~/Projects/foo-dockerside by side, they corrupt the neighbour. That is not carelessness; there is nowhere to look up which files carry the path and how it must be matched. The layout is internal and undocumented, so each tool encodes a different guess at it, and each guess ages differently as new state directories appear.~/.claude/projects/<old-slug>/. A user who doesn't already suspect what happened will not search for a tool, and by then may have started working in a fresh project dir. The startup hint proposed above costs far less than the command and removes most of the harm on its own.~/.claude— including~/.claude.json, which holds MCP server configs and trust flags. Recommending one to a colleague is not a comfortable thing to do, and in many workplaces it isn't an allowed one.I migrated my own project by hand rather than use any of these, and covered the same set as the table's last two columns combined, minus
file-history. That took a session's worth of investigation to establish — for something that is onemvfrom the user's point of view.Hi 👋 human here with a solution that Claude helped me build. I'll let Claude explain:
A case the table doesn't cover: the whole home directory moves, and the destination isn't empty
I moved to a new Mac with a different username (
/Users/<old>→/Users/<new>): about 60 sessions across 30 projects. By the time I noticed the missing history, I'd already worked in several projects on the new machine. All the tools compared above move one project into an empty slug. Merging into a slug that already has work in it needs four extra cases handled.claude project mv(or a merge mode of it) will need them too:uuids tells the two cases apart. If the old copy's set is contained in the new copy's, it's already migrated. Otherwise the copies have diverged, and the only safe move is to leave both alone and say so./resume. I skip an old session when a session here has the samecustomTitleand later activity.uuidsequences. Separately, one session ID appeared under both a project's slug and one of its worktree slugs. Migrating everything gives duplicate picker entries. Keeping the copy whose project directory still exists fixes that. The dropped copy'stool-results/files still have to stay at the path its transcript links to.~/.claude.jsonentries. When the new path already has an entry, copying the old one over resets permissions and trust set on the new machine. Adding only where no entry exists avoids that. For me, that adds 16 of my 24 old entries and leaves 8 alone, because those projects already had entries on the new machine. Myhistory.jsonlhad the same problem as in the table: all 30 entries still pointed at the old home.My first version also had the sibling-prefix bug from the issue body (a plain
str.replaceof the home path). I only caught it after reading this issue.For the startup hint, the case to cover is "a slug exists for a path under a different home that doesn't resolve", not only "the current slug is empty". In a home move, the current slug often isn't empty.
What worked for me, in case it helps anyone before this lands: https://github.com/ip2k/claude-rehome. It's one standard-library Python file. It does a dry run by default, never overwrites anything, and waits for running sessions to exit before touching
~/.claude.jsonandhistory.jsonl. Each case above has a test. It does not cover profiles,file-history/,todos/orjobs/; the tools in the table above do.Been wanting this for months! (Currently rolling my own tooling for this, but would be much nicer to have a built in feature)