Claude Code migration guide

A reusable guide for migrating Claude Code configuration, memories, sessions, and project repos between Windows machines.

Written by Claude Code 2.1.196 in June 2026.


What Claude Code stores and where

Global config — %USERPROFILE%\.claude\

Path What it is Portable?
settings.json API tokens, model preferences, theme, global permissions allowlist ✅ Essential
CLAUDE.md Global instructions loaded every session ✅ Essential
commands\ Custom slash commands (.md files) ✅ Essential
memory\ Global memories (MEMORY.md index + individual .md files) ✅ Essential
projects\<path-encoded>\memory\ Project-specific memories ✅ Essential
projects\<path-encoded>\<uuid>\.jsonl Session transcripts ✅ Best-effort
projects\<path-encoded>\<uuid>\subagents\ Subagent transcripts for that session ✅ Best-effort
projects\<path-encoded>\<uuid>\tool-results\ Cached tool outputs for that session ✅ Best-effort
sessions\, cache\, paste-cache\ Ephemeral runtime state ❌ Skip
shell-snapshots\, session-env\, daemon\ Ephemeral runtime state ❌ Skip
file-history\, backups\ Edit snapshots, session backups ❌ Skip
plugins\, ide\, usage-data\ Auto-populated on first use ❌ Skip

Project-level config — inside each repo

Path What it is
<repo>\.claude\settings.json Shared project settings (committed to git)
<repo>\.claude\settings.local.json Local project permissions (not committed)
<repo>\.claude\commands\ Project-specific custom commands
<repo>\CLAUDE.md Project instructions

These travel with the repo — if you bring your git clones, these come along automatically.
The only exception is settings.local.json, which is gitignored. Make sure to include it
in your transfer if it exists.

How project folder names are encoded

Claude Code maps repo paths to folder names under %USERPROFILE%\.claude\projects\.
The encoding rule:

  • Replace :\ with --
  • Replace every remaining \ with -

Examples:

  • C:\dev\my-appC--dev-my-app
  • F:\work\repos\frontendF--work-repos-frontend
  • D:\code\my-project\sub-repoD--code-my-project-sub-repo

This encoding is what connects a repo directory to its sessions and project memories.


Phase 1: Prepare and archive on the old machine

1.1 Clean up git worktrees (optional but recommended)

Claude Code creates worktrees under <repo>\.claude\worktrees\. These are full working
copies that can be very large. The session transcripts that used them are stored separately
in %USERPROFILE%\.claude\projects\ and are preserved regardless.

cd <repo>
git worktree list           # Review what exists
git worktree remove <name>  # Remove ones you don't need

1.2 Archive Claude Code global config

$rar = "C:\Program Files\WinRAR\rar.exe"
$staging = "$env:USERPROFILE\Desktop\migration"
New-Item -ItemType Directory -Force $staging

& $rar a -r -ep1 "$staging\dot-claude.rar" "$env:USERPROFILE\.claude\*"

# Also grab the state file (sits in %USERPROFILE% directly, not inside .claude).
# Contains onboarding state, feature flags, and project trust — avoids
# first-launch setup prompts on the new machine.
Copy-Item "$env:USERPROFILE\.claude.json" "$staging\.claude.json"

1.3 Archive project repos

Archive your repos, excluding build artifacts to save space.

# Exclude bin, obj, packages, node_modules, artifacts, runtime — adjust as needed
& $rar a -r -ep1 `
    -x*\bin\ -x*\bin\* `
    -x*\obj\ -x*\obj\* `
    -x*\packages\ -x*\packages\* `
    -x*\node_modules\ -x*\node_modules\* `
    -x*\artifacts\ -x*\artifacts\* `
    -x*\runtime\ -x*\runtime\* `
    "$staging\my-repos.rar" "C:\dev\repos\*"

WinRAR -x flags: Use pairs — -x*\name\ excludes the directory entry itself,
-x*\name\* excludes its contents. Both are needed.

1.4 Transfer

Copy the .rar files to the new machine via USB drive, network share, cloud storage, etc.


Phase 2: Set up the new machine

2.1 Install Claude Code

irm https://claude.ai/install.ps1 | iex
claude --version
The installer may warn that %USERPROFILE%\.local\bin is not in your PATH. If claude --version doesn’t work after installing, add it:
# Add Claude Code to the user PATH (persists across sessions, no admin needed)
$binPath = "$env:USERPROFILE\.local\bin"
$currentPath = [System.Environment]::GetEnvironmentVariable('Path', 'User')
if ($currentPath -notlike "*$binPath*") {
    [System.Environment]::SetEnvironmentVariable('Path', "$currentPath;$binPath", 'User')
    Write-Host "Added $binPath to user PATH. Restart your terminal to pick it up."
} else {
    Write-Host "$binPath is already in PATH."
}
After running this, restart your terminal (or open a new tab) for the change to take effect.

2.2 Extract archives

Extract the Claude Code config to the same user profile path. Extract repos to their new
location.

$rar = "C:\Program Files\WinRAR\rar.exe"

# This is where the transferred files were placed
$transfer = "$env:USERPROFILE\Desktop\transfer"

# Claude Code global config — same user profile path
& $rar x "$transfer\dot-claude.rar" "$env:USERPROFILE\.claude\"

# Claude Code state file
Copy-Item "$<transfer\.claude.json" "$env:USERPROFILE\.claude.json"

# Repos — new location
New-Item -ItemType Directory -Force "<new-repo-path>"
& $rar x "$transfer\my-repos.rar" "<new-repo-path>\"

Replace $transfer with wherever you put the .rar files on the new machine.

2.3 Preserve old sessions and project memories (optional)

This step is optional. Skip it and everything works for day-to-day use — Claude Code
creates new project folders based on the new paths, global memories and settings carry over,
and new sessions work normally. Here’s what you lose by skipping:

  • Old sessions won’t appear in /resume. Session transcripts are stored under
    %USERPROFILE%\.claude\projects\<old-path-encoded>\. Starting Claude from the new path
    makes it look in <new-path-encoded>\ instead — a different folder. The old transcripts
    remain on disk but are orphaned.
  • Project-specific memories won’t be picked up. Memories under
    %USERPROFILE%\.claude\projects\<old-path-encoded>\memory\ won’t be found by sessions
    started from the new path.
    (Global memories in %USERPROFILE%\.claude\memory\ are unaffected.)

If you want to preserve these, pick one of the two options below.

Option A: Create junctions for old paths

Junctions make the old paths resolve to the new locations. This is the most complete
solution — everything works transparently, including file path references inside old
session transcripts.

# For each old repo root, create a junction to the new location.
# Example: old path was C:\dev\repos, new path is D:\work\repos
New-Item -ItemType Directory -Force "C:\dev"
New-Item -ItemType Junction -Path "C:\dev\repos" -Target "D:\work\repos"

Verify:

# These should show the same content:
ls C:\dev\repos
ls D:\work\repos

What this gives you:

Works?
Old sessions appear in /resume
Project memories load automatically
File paths inside old transcripts resolve to real files
/resume and continue working in an old session ✅ (best-effort — git state may have changed)

Trade-off: Phantom directories on the new machine that only exist for backward
compatibility. Junctions are lightweight (no disk space, no copying), but they’re an
extra thing to know about. You can remove them later — the only consequence is losing
old sessions in /resume.

Option B: Rename project folders to match new paths

Rename the path-encoded project folders under %USERPROFILE%\.claude\projects\ so they
match the new repo locations. This makes old sessions and project memories visible without
creating any junction directories.

Step 1: Determine old and new folder names.

Apply the encoding rule (replace :\ with --, then every \ with -) to both old and
new paths:

Old path Old folder name New path New folder name
C:\dev\repos\my-app C--dev-repos-my-app D:\work\my-app D--work-my-app

List your actual project folders to see what needs renaming:

Get-ChildItem "$env:USERPROFILE\.claude\projects" -Directory | Select-Object -ExpandProperty Name

Step 2: Rename the folders.

# Repeat for each project folder
$projects = "$env:USERPROFILE\.claude\projects"
Rename-Item "$projects\<old-folder-name>" "<new-folder-name>"

Step 3 (optional): Tag old sessions as migrated.

Prefix old session titles with [MIGRATED] so they’re easy to distinguish from new
sessions in /resume. The title is stored as a JSON line in each .jsonl transcript file.

# Sessions can have auto-generated titles ("ai-title" / "aiTitle") or
# user-renamed titles ("custom-title" / "customTitle", "agent-name" / "agentName").
Get-ChildItem "$env:USERPROFILE\.claude\projects" -Filter "*.jsonl" -Recurse |
    ForEach-Object {
        $content = [System.IO.File]::ReadAllText($_.FullName)
        $original = $content
        $content = $content -replace '"aiTitle"\s*:\s*"', '"aiTitle":"[MIGRATED] '
        $content = $content -replace '"customTitle"\s*:\s*"', '"customTitle":"[MIGRATED] '
        $content = $content -replace '"agentName"\s*:\s*"', '"agentName":"[MIGRATED] '
        if ($content -ne $original) {
            [System.IO.File]::WriteAllText($_.FullName, $content)
            Write-Host "Tagged: $($_.FullName)"
        }
    }

After this, /resume shows old sessions as e.g.:

  • [MIGRATED] Add authentication endpoint
  • [MIGRATED] Fix database migration bug

Step 4 (optional): Rewrite file paths inside old session transcripts.

Session transcripts contain hardcoded paths from the old machine (e.g. file reads, tool
results, cwd fields). You can rewrite them so old sessions are fully functional — not
just browsable.

Paths in JSONL are JSON-escaped with double backslashes (F:\\dev\\repos\\my-app), so the
replacement must use the escaped form. Define one replacement per repo root that moved:

# Define path replacements: old (JSON-escaped) → new (JSON-escaped)
# Add one entry per repo root that changed path.
$replacements = @(
    @{ Old = 'C:\\\\dev\\\\repos';  New = 'D:\\\\work\\\\repos' }
    # @{ Old = 'F:\\\\git\\\\github'; New = 'F:\\\\dev\\\\github' }
    # Add more as needed...
)

Get-ChildItem "$env:USERPROFILE\.claude\projects" -Filter "*.jsonl" -Recurse |
    ForEach-Object {
        $content = [System.IO.File]::ReadAllText($_.FullName)
        $original = $content
        foreach ($r in $replacements) {
            $content = $content.Replace($r.Old, $r.New)
        }
        if ($content -ne $original) {
            [System.IO.File]::WriteAllText($_.FullName, $content)
            Write-Host "Updated paths: $($_.FullName)"
        }
    }

Why this is safe:

The replacement is a simple prefix substitution on JSON-escaped
absolute paths (e.g. C:\\dev\\reposD:\\work\\repos). These are specific enough
that false positives are essentially impossible — they only appear as file path references
in tool calls and results. The double-backslash JSON escaping means regular text content
won’t accidentally match.

What this gives you:

Works?
Old sessions appear in /resume
Old sessions clearly marked as migrated ✅ (if you did step 3)
Project memories load automatically
File paths inside old transcripts resolve to real files ✅ (if you did step 4)
/resume and continue working in an old session ✅ (best-effort — git state may have changed)

With steps 3 and 4, Option B gives the same result as Option A — just without the
junction directories.

2.4 Fix git worktree cross-references

Git worktrees store absolute paths internally. If you brought worktrees along, repair them
after extracting:

cd <repo-with-worktrees>
git worktree repair
git worktree list    # Verify all show correct paths

2.5 Update project paths and trust in .claude.json

The %USERPROFILE%\.claude.json file contains per-project state keyed by repo path (using forward slashes, e.g. "C:/dev/repos") and some values with escaped backslashes (e.g. "C:\\dev\\repos\\..."). Both need updating to match the new locations.

It also controls project trust — Claude Code won’t apply project-level settings (.claude\settings.local.json.claude\settings.jsonCLAUDE.md) until hasTrustDialogAccepted is true for that project. Since these are your own migrated projects, the script below sets them all to trusted.

$file = "$env:USERPROFILE\.claude.json"
$content = [System.IO.File]::ReadAllText($file)

# Update project paths — add one pair per repo root that changed.
# Forward-slash form (used in JSON property keys):
$content = $content.Replace('C:/old/path', 'D:/new/path')
# Escaped-backslash form (used in some JSON values):
$content = $content.Replace('C:\\old\\path', 'D:\\new\\path')

# Trust all projects
$content = $content.Replace('"hasTrustDialogAccepted": false', '"hasTrustDialogAccepted": true')

[System.IO.File]::WriteAllText($file, $content)
Write-Host "Updated paths and trust in .claude.json"

Alternatively, skip the path updates and start Claude Code once from each project directory — it will create new project entries and prompt you to accept the trust dialog interactively.

2.6 Review API tokens and secrets

Check settings.json for any API tokens, PATs, or service credentials. Update any that
are machine-specific, rotated, or expired:

notepad "$env:USERPROFILE\.claude\settings.json"

Phase 3: Verify

3.1 Start Claude Code and check basics

cd <your-repo>
claude

Check that:

  • Memories load automatically (if you have a CLAUDE.md that references them)
  • Custom commands are available (type / and look for your commands)
  • Model preference is preserved
  • Permissions allowlist works as expected

3.2 Check old sessions (if you did step 2.3)

/resume

Should list your old sessions. Select one to browse the conversation history.


Quick reference: what to bring

Must have

Item What it does
%USERPROFILE%\.claude\settings.json API tokens, model, theme, permissions
%USERPROFILE%\.claude\CLAUDE.md Global instructions
%USERPROFILE%\.claude\commands\ Custom slash commands
%USERPROFILE%\.claude\memory\ Global memories (preferences, style, team info)
%USERPROFILE%\.claude\projects\*\memory\ Project-specific memories
Repo \.claude\settings.local.json Per-project local permissions (gitignored)

Nice to have

Item What it does
%USERPROFILE%\.claude\projects\*\*.jsonl Session transcripts (browsable via /resume)
%USERPROFILE%\.claude\projects\*\*\subagents\ Subagent transcripts

Skip

Item Why
sessions\, cache\, daemon\ Runtime state, auto-recreated
shell-snapshots\, session-env\ Environment captures, stale after move
paste-cache\, file-history\, backups\ Ephemeral, auto-recreated
plugins\, ide\, usage-data\ Auto-populated on first use

Troubleshooting

/resume doesn’t show old sessions

  • Verify the project folder exists and has the correct name:
    Get-ChildItem "$env:USERPROFILE\.claude\projects" and check the encoding matches your
    repo path
  • If you used junctions: verify they resolve (Test-Path <old-path> should be True)
  • If you renamed folders: verify the new name matches the path encoding exactly

Git worktrees are broken after the move

cd <repo>
git worktree repair          # Fixes absolute path cross-references (Git 2.30+)
git worktree list            # Verify
git worktree remove <name> --force   # Remove if unrepairable

Permission errors on junctions

Junctions on NTFS don’t require admin rights. If you get errors, ensure:

  • The parent directory exists (e.g. C:\dev\)
  • The target directory exists
  • There’s no existing file/folder at the junction path

API tokens don’t work

Tokens in settings.json may be machine-specific or expired. Generate new ones and update
the file.