[FEATURE] `claude project mv` — one command to re-point a project's state after its directory moves (transcripts, memory, prompt history, session registry, profiles)

Status Open
Reported on v2.1.278
Maintainer reply None cached
Activity 3 comments · opened Sep 22, 2026

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:

  1. 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 .git inode), 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.
  2. Warn on orphans. claude doctor could 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.json project entry, or the /resume picker (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-docker exists 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.json is 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.

View original on GitHub ↗

3 Comments

likemusic · 22 days ago

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.json project entry | ❌ | ❌ | ❌ | ✅ | ✅ |
| history.jsonl (prompt history) | ❌ | ✅ | ✅ | ✅ | ✅ |
| sessions/<pid>.json registry | ❌ | ❌ | ❌ | read-only, for the liveness check | ✅ via generic sweep |
| file-history/, todos/, shell-snapshots/, debug/ | ❌ | ✅ | ✅ | n/a by design (session-keyed) | ✅, incl. renaming file-history blobs whose names are sha256(path) |
| jobs/, plans/, other top-level state | ❌ | ❌ | ❌ | n/a by design | ✅ generic sweep of everything but projects/, 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 the cwd JSON 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-mv is the only one that understands profiles, and NotebookNomad/claude-move is the only one that renames file-history blobs 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:

  1. Every author reverse-engineered a different subset, and the two unbounded string replacements (sed, str.replace) are exactly the sibling-prefix bug from the issue body — with ~/Projects/foo and ~/Projects/foo-docker side 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.
  2. None of them is discoverable at the moment it is needed. The failure is silent: the model simply has no memory of the project, and nothing points at ~/.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.
  3. Trust. These are single-file scripts with 0–6 stars, unsigned and unversioned, asking to bulk-rewrite ~/.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 one mv from the user's point of view.

ip2k · 21 days ago

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:

  1. The same session ID on both sides. Copying over it loses the new machine's work, and "newest wins" loses the old machine's. Comparing each copy's message 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.
  2. Work continued under a new session ID. If a session was picked up on the new machine under a new ID (a fork, or a fresh session given the same name), the migrated old copy becomes a stale duplicate in /resume. I skip an old session when a session here has the same customTitle and later activity.
  3. The same conversation stored under two IDs. A session that started in one directory and moved to another partway through was on disk twice, in two slug directories, with identical uuid sequences. 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's tool-results/ files still have to stay at the path its transcript links to.
  4. Merging ~/.claude.json entries. 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. My history.jsonl had 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.replace of 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.json and history.jsonl. Each case above has a test. It does not cover profiles, file-history/, todos/ or jobs/; the tools in the table above do.

wmayner · 11 days ago

Been wanting this for months! (Currently rolling my own tooling for this, but would be much nicer to have a built in feature)