Move Claude Desktop Code sessions to another account on the same Mac, pins included. Dry-run first, undoable. Unofficial.
Go
4
13 commits
updated Oct 12, 2026
Unofficial community tool. Not affiliated with or endorsed by Anthropic.
Move your Claude Desktop Code sessions to another account on the same Mac, so the sidebar shows them again.
When you switch Claude accounts (personal to Team, for example), the Code tab in Claude Desktop starts empty. Nothing was deleted: as observed in Claude Desktop (October 2026), it keeps one sidebar store per account and organization and only reads the store of the account you are signed in to. claude-migrate copies the session records from the old account's store to the new one, keeps their IDs (so pins carry over), clears the links that belong to the old account, and never touches transcripts or existing files.
A Desktop Code session has two halves. Only one of them is tied to an account. The Desktop storage described here is as observed in Claude Desktop (October 2026); it is undocumented and can change.
flowchart LR
subgraph shared["Account-independent"]
T["~/.claude/projects/#lt;project#gt;/#lt;session#gt;.jsonl<br/>transcript: the conversation"]
end
subgraph old["Old account store"]
R1["claude-code-sessions/#lt;acct#gt;/#lt;org#gt;/local_#lt;id#gt;.json<br/>sidebar record"]
end
subgraph new["New account store"]
R2["claude-code-sessions/#lt;acct#gt;/#lt;org#gt;/local_#lt;id#gt;.json<br/>sidebar record"]
end
R1 -- "claude-migrate copies<br/>(same file name and ID)" --> R2
R1 -. points to .-> T
R2 -. points to .-> T
claude --resume sees it regardless of who is signed in.<account>/<org> folder. This is what "disappears" after a switch.claude-migrate copies records only. Transcripts are never read for content, moved or modified.apply is additive only and never overwrites a file.undo removes exactly what a run created, and only if the files are unchanged.--include-orphans copies them anyway.deleted_<id> marker, and with kept IDs that marker names the same session.--json output on every command except version. Standard library only, no network access.Homebrew cask:
brew install --cask onlyilkr/tap/claude-migrate
The binary is not signed or notarized, so the cask removes the macOS quarantine flag from it on install; without that, Gatekeeper would refuse to run it. If you would rather not have a cask do that, use go install (builds on your Mac, no workaround needed):
go install github.com/onlyilkr/claude-migrate/cmd/claude-migrate@latest
Or download a release binary (darwin/arm64 or darwin/amd64) from the releases page and verify it against the published checksums:
shasum -a 256 -c --ignore-missing checksums.txt # run next to the downloaded archive
claude-migrate # same as: claude-migrate plan
With several candidate stores, run claude-migrate doctor to list them.claude-migrate apply --from 1a2b3c4d/5e6f7a8b
Changed your mind? claude-migrate undo removes exactly the files the last run created.
| Command | What it does | Writes? |
|---|---|---|
claude-migrate / plan | Shows what would be copied and what needs manual follow-up. Picks the source for you when there is only one candidate; asks on a TTY otherwise. | No |
apply --from X | Copies the records and writes a manifest to ~/.claude-migrate/runs/. --from is required. | Yes |
undo [manifest] | Removes the files a run created (latest run by default), skipping any you changed since. Without a path it refuses, instead of picking an older run, when a newer run file is unreadable. | Yes (deletes) |
pins | Lists the pinned source sessions that are being copied or are already in the target (skipped ones, whether no transcript, deleted or a name conflict, are not listed), each as pinned in target, copied under a different ID; pin it again in Desktop or not migrated yet. | No |
doctor | Desktop state, signed-in account, whether Desktop last used the same account, stores, cleanupPeriodDays. | No |
version | Prints the version. | No |
| Flag | Applies to | Meaning |
|---|---|---|
--from <acct>/<org> | plan, apply, pins | Source store. Unique prefixes work (1a2b3c4d/5e6f7a8b). |
--to <acct>/<org> | plan, apply, pins | Target store. Defaults to the account Claude Code is signed in to (~/.claude.json). Passing it skips the Desktop account cross-check. |
--include-orphans | plan, apply | Also copy sessions whose transcript is missing. They open empty in Desktop. |
--force | undo | Also remove files that changed since the run. Bypasses no other protection. |
--json | all but version | Machine-readable output. |
| Code | Meaning |
|---|---|
| 0 | OK |
| 1 | Error (I/O failure, incomplete undo) |
| 2 | Usage error: bad flag, unresolvable or ambiguous store, source equals target, no signed-in account, a ~/.claude.json that cannot be read or parsed, undo of a manifest path that does not exist, undo without a path when there are no runs or a newer run file is unreadable, or apply with an account mismatch (see below) |
| 3 | Claude Desktop is running, or cannot be checked (SSH session) |
| 4 | Unrecognized Desktop folder layout, or the target store does not exist |
| Field | Result |
|---|---|
bridgeSessionIds | Emptied. These are the old account's cloud session IDs. |
scheduledTaskId | Removed. It points at a task of the old account. |
sessionId and file name | Kept. The copy has the same local_<id>.json name as the source. |
| Everything else | Value-preserved: each field's original JSON is kept verbatim (keys are re-sorted and re-indented), including fields this tool does not know about. |
Unknown fields never block a run; plan lists them for information.
apply and undo write.link(2), which fails atomically if the name exists, so it never overwrites, even under a race. If a write fails after its manifest entry, the entry is retracted so undo cannot treat whatever sits at that path as ours. If the target already holds a file of that name that belongs to a different session, or an entry of that name that is not a regular file (a symlink or folder), the record is skipped and reported (name_conflicts).undo only deletes files named in the manifest, inside the target store, whose hash still matches. A hand-edited manifest cannot point it elsewhere. A file that a newer run recorded again (same path, same kept ID) belongs to that run: undoing the older run keeps it and reports it as superseded ("kept for a newer run"). Newer runs that cannot be read are listed, because they protect nothing.apply and undo refuse while Desktop runs. The check asks Launch Services about the current GUI login session, so run the tool in a Terminal on the Mac itself; over SSH it cannot see Desktop and refuses (exit 3). With --home, the guard still applies whenever the target is, by file identity, a store under your real home (symlinks, ., case variants included).config.json. If you do not pass --to and that account differs from the one in ~/.claude.json, apply stops (exit 2) and plan/pins print a warning on stderr. Pass --to <account>/<org> to choose explicitly.cleanupPeriodDays (default 30 days) are deleted by Claude Code. plan warns when the setting is absent.doctor after an update, and expect fail-closed behavior rather than silent damage.A session I deleted in the new account was not copied again. By design: the target's deleted_<id> marker is respected and the plan lists it as "deleted in target". A session that an older tool copied under a different ID and that you then deleted cannot be matched this way, because its marker names the other ID.
The account cross-check did not catch a wrong organization. Desktop's config.json records only the last account (lastKnownAccountUuid), not the organization. If one account has several organizations, check the Target: line of plan or pass --to <account>/<org>.
Can I delete the sessions from the old account afterwards? Wait until you have opened a few migrated sessions in the new account. Both copies share the same session ID and transcript, and how Desktop treats shared state when one copy is deleted is not documented.
The sidebar is still empty after apply. As observed, Desktop writes its in-memory list back on exit. Quit it fully with Cmd+Q (not just the window), relaunch, and make sure you are signed in to the target account.
~/.claude.json is signed in to account … but Claude Desktop last used account …. The two disagree about which account is current. Check claude-migrate doctor, then pass --to <account>/<org> for the store you actually want.
"no transcript (skipped)" is not zero. Those records point at a transcript that is no longer in ~/.claude/projects (often removed by cleanupPeriodDays). In Desktop they open empty. Copy them anyway with --include-orphans, or leave them behind.
How do pins work? As observed in Claude Desktop (October 2026), pins are stored as a single list of local_<id> values in claude_desktop_config.json, shared by all accounts. Because the copy keeps its ID, it is pinned as soon as it appears. The tool reads that list (read-only) to decide which source sessions are pinned; a record's own isStarred flag is only the fallback when the list cannot be read. claude-migrate pins shows each pinned source session as pinned in target or not migrated yet. A session copied by an older tool under a new ID counts as pinned in target if you already pinned that copy again, and as copied under a different ID; pin it again in Desktop otherwise; plan counts only the latter in its pin follow-up line.
A record was skipped as a name conflict. The target already has a file with that name for a different session. It is left untouched and listed in the plan.
To see what a run does without touching your real Desktop data, build a scratch directory laid out like a home and point --home at it:
SCRATCH=$(mktemp -d)
mkdir -p "$SCRATCH/Library/Application Support/Claude"
cp -R ~/Library/Application\ Support/Claude/claude-code-sessions "$SCRATCH/Library/Application Support/Claude/"
cp ~/Library/Application\ Support/Claude/config.json "$SCRATCH/Library/Application Support/Claude/" # enables the account cross-check
# Copy only the pin list (jq ships with macOS 15+): the full claude_desktop_config.json can hold MCP server secrets.
jq '{preferences:{epitaxyPrefs:{"starred-local-code-sessions":.preferences.epitaxyPrefs["starred-local-code-sessions"]}}}' \
~/Library/Application\ Support/Claude/claude_desktop_config.json > "$SCRATCH/Library/Application Support/Claude/claude_desktop_config.json"
cp ~/.claude.json "$SCRATCH/"
claude-migrate plan --home "$SCRATCH" --include-orphans
claude-migrate apply --home "$SCRATCH" --from <old-account>/<old-org> --include-orphans
claude-migrate undo --home "$SCRATCH"
The Desktop check is skipped for a home that is not yours, and nothing outside the scratch directory is touched. ~/.claude/projects need not be copied; --include-orphans stands in for the transcripts you left out. Delete the scratch directory when done: the copy of ~/.claude.json holds your account details. --home is a testing flag hidden from claude-migrate help.
Other community projects that touch the same problem space (none is affiliated with this one). Descriptions summarize each project's own README as of October 2026:
| Tool | What it does |
|---|---|
| claude-code-export-import | Exports and imports Claude Code sessions between machines, accounts and app builds; rewrites transcripts. |
| transfer-claude-sessions | Copies Desktop sessions to a new account with dry run, backup and undo; Windows PowerShell. |
| claude-session-migrate | Claude Code skill and script that copies Desktop sidebar records to the new account; Windows-focused, backs up the sessions folder first. |
| claude-account-migration | Write-up of where Desktop keeps sessions and why a switch hides them, with helper scripts; macOS and Windows. |
| cowork-migrate | Moves Cowork sessions between Macs. |
| claude-folder-move | Retargets Claude Code sessions when a project folder moves. |
make check # test (race), vet, gofmt check, staticcheck
make build # bin/claude-migrate
make snapshot # local GoReleaser build, nothing published
See CONTRIBUTING.md for the TDD workflow and fixture rules. To add a regression fixture from your own store without leaking data, use go run ./scripts/anonymize -src <store dir> -out <dir> and check the output with the privacy grep described in CONTRIBUTING.md.
Contributions are welcome; read CONTRIBUTING.md first. Report vulnerabilities privately as described in SECURITY.md, not in a public issue. Release notes are in CHANGELOG.md.
Move Claude Desktop Code sessions to another account on the same Mac, pins included. Dry-run first, undoable. Unofficial.
Go
4
13 commits
updated Oct 12, 2026
Unofficial community tool. Not affiliated with or endorsed by Anthropic.
Move your Claude Desktop Code sessions to another account on the same Mac, so the sidebar shows them again.
When you switch Claude accounts (personal to Team, for example), the Code tab in Claude Desktop starts empty. Nothing was deleted: as observed in Claude Desktop (October 2026), it keeps one sidebar store per account and organization and only reads the store of the account you are signed in to. claude-migrate copies the session records from the old account's store to the new one, keeps their IDs (so pins carry over), clears the links that belong to the old account, and never touches transcripts or existing files.
A Desktop Code session has two halves. Only one of them is tied to an account. The Desktop storage described here is as observed in Claude Desktop (October 2026); it is undocumented and can change.
flowchart LR
subgraph shared["Account-independent"]
T["~/.claude/projects/#lt;project#gt;/#lt;session#gt;.jsonl<br/>transcript: the conversation"]
end
subgraph old["Old account store"]
R1["claude-code-sessions/#lt;acct#gt;/#lt;org#gt;/local_#lt;id#gt;.json<br/>sidebar record"]
end
subgraph new["New account store"]
R2["claude-code-sessions/#lt;acct#gt;/#lt;org#gt;/local_#lt;id#gt;.json<br/>sidebar record"]
end
R1 -- "claude-migrate copies<br/>(same file name and ID)" --> R2
R1 -. points to .-> T
R2 -. points to .-> T
claude --resume sees it regardless of who is signed in.<account>/<org> folder. This is what "disappears" after a switch.claude-migrate copies records only. Transcripts are never read for content, moved or modified.apply is additive only and never overwrites a file.undo removes exactly what a run created, and only if the files are unchanged.--include-orphans copies them anyway.deleted_<id> marker, and with kept IDs that marker names the same session.--json output on every command except version. Standard library only, no network access.Homebrew cask:
brew install --cask onlyilkr/tap/claude-migrate
The binary is not signed or notarized, so the cask removes the macOS quarantine flag from it on install; without that, Gatekeeper would refuse to run it. If you would rather not have a cask do that, use go install (builds on your Mac, no workaround needed):
go install github.com/onlyilkr/claude-migrate/cmd/claude-migrate@latest
Or download a release binary (darwin/arm64 or darwin/amd64) from the releases page and verify it against the published checksums:
shasum -a 256 -c --ignore-missing checksums.txt # run next to the downloaded archive
claude-migrate # same as: claude-migrate plan
With several candidate stores, run claude-migrate doctor to list them.claude-migrate apply --from 1a2b3c4d/5e6f7a8b
Changed your mind? claude-migrate undo removes exactly the files the last run created.
| Command | What it does | Writes? |
|---|---|---|
claude-migrate / plan | Shows what would be copied and what needs manual follow-up. Picks the source for you when there is only one candidate; asks on a TTY otherwise. | No |
apply --from X | Copies the records and writes a manifest to ~/.claude-migrate/runs/. --from is required. | Yes |
undo [manifest] | Removes the files a run created (latest run by default), skipping any you changed since. Without a path it refuses, instead of picking an older run, when a newer run file is unreadable. | Yes (deletes) |
pins | Lists the pinned source sessions that are being copied or are already in the target (skipped ones, whether no transcript, deleted or a name conflict, are not listed), each as pinned in target, copied under a different ID; pin it again in Desktop or not migrated yet. | No |
doctor | Desktop state, signed-in account, whether Desktop last used the same account, stores, cleanupPeriodDays. | No |
version | Prints the version. | No |
| Flag | Applies to | Meaning |
|---|---|---|
--from <acct>/<org> | plan, apply, pins | Source store. Unique prefixes work (1a2b3c4d/5e6f7a8b). |
--to <acct>/<org> | plan, apply, pins | Target store. Defaults to the account Claude Code is signed in to (~/.claude.json). Passing it skips the Desktop account cross-check. |
--include-orphans | plan, apply | Also copy sessions whose transcript is missing. They open empty in Desktop. |
--force | undo | Also remove files that changed since the run. Bypasses no other protection. |
--json | all but version | Machine-readable output. |
| Code | Meaning |
|---|---|
| 0 | OK |
| 1 | Error (I/O failure, incomplete undo) |
| 2 | Usage error: bad flag, unresolvable or ambiguous store, source equals target, no signed-in account, a ~/.claude.json that cannot be read or parsed, undo of a manifest path that does not exist, undo without a path when there are no runs or a newer run file is unreadable, or apply with an account mismatch (see below) |
| 3 | Claude Desktop is running, or cannot be checked (SSH session) |
| 4 | Unrecognized Desktop folder layout, or the target store does not exist |
| Field | Result |
|---|---|
bridgeSessionIds | Emptied. These are the old account's cloud session IDs. |
scheduledTaskId | Removed. It points at a task of the old account. |
sessionId and file name | Kept. The copy has the same local_<id>.json name as the source. |
| Everything else | Value-preserved: each field's original JSON is kept verbatim (keys are re-sorted and re-indented), including fields this tool does not know about. |
Unknown fields never block a run; plan lists them for information.
apply and undo write.link(2), which fails atomically if the name exists, so it never overwrites, even under a race. If a write fails after its manifest entry, the entry is retracted so undo cannot treat whatever sits at that path as ours. If the target already holds a file of that name that belongs to a different session, or an entry of that name that is not a regular file (a symlink or folder), the record is skipped and reported (name_conflicts).undo only deletes files named in the manifest, inside the target store, whose hash still matches. A hand-edited manifest cannot point it elsewhere. A file that a newer run recorded again (same path, same kept ID) belongs to that run: undoing the older run keeps it and reports it as superseded ("kept for a newer run"). Newer runs that cannot be read are listed, because they protect nothing.apply and undo refuse while Desktop runs. The check asks Launch Services about the current GUI login session, so run the tool in a Terminal on the Mac itself; over SSH it cannot see Desktop and refuses (exit 3). With --home, the guard still applies whenever the target is, by file identity, a store under your real home (symlinks, ., case variants included).config.json. If you do not pass --to and that account differs from the one in ~/.claude.json, apply stops (exit 2) and plan/pins print a warning on stderr. Pass --to <account>/<org> to choose explicitly.cleanupPeriodDays (default 30 days) are deleted by Claude Code. plan warns when the setting is absent.doctor after an update, and expect fail-closed behavior rather than silent damage.A session I deleted in the new account was not copied again. By design: the target's deleted_<id> marker is respected and the plan lists it as "deleted in target". A session that an older tool copied under a different ID and that you then deleted cannot be matched this way, because its marker names the other ID.
The account cross-check did not catch a wrong organization. Desktop's config.json records only the last account (lastKnownAccountUuid), not the organization. If one account has several organizations, check the Target: line of plan or pass --to <account>/<org>.
Can I delete the sessions from the old account afterwards? Wait until you have opened a few migrated sessions in the new account. Both copies share the same session ID and transcript, and how Desktop treats shared state when one copy is deleted is not documented.
The sidebar is still empty after apply. As observed, Desktop writes its in-memory list back on exit. Quit it fully with Cmd+Q (not just the window), relaunch, and make sure you are signed in to the target account.
~/.claude.json is signed in to account … but Claude Desktop last used account …. The two disagree about which account is current. Check claude-migrate doctor, then pass --to <account>/<org> for the store you actually want.
"no transcript (skipped)" is not zero. Those records point at a transcript that is no longer in ~/.claude/projects (often removed by cleanupPeriodDays). In Desktop they open empty. Copy them anyway with --include-orphans, or leave them behind.
How do pins work? As observed in Claude Desktop (October 2026), pins are stored as a single list of local_<id> values in claude_desktop_config.json, shared by all accounts. Because the copy keeps its ID, it is pinned as soon as it appears. The tool reads that list (read-only) to decide which source sessions are pinned; a record's own isStarred flag is only the fallback when the list cannot be read. claude-migrate pins shows each pinned source session as pinned in target or not migrated yet. A session copied by an older tool under a new ID counts as pinned in target if you already pinned that copy again, and as copied under a different ID; pin it again in Desktop otherwise; plan counts only the latter in its pin follow-up line.
A record was skipped as a name conflict. The target already has a file with that name for a different session. It is left untouched and listed in the plan.
To see what a run does without touching your real Desktop data, build a scratch directory laid out like a home and point --home at it:
SCRATCH=$(mktemp -d)
mkdir -p "$SCRATCH/Library/Application Support/Claude"
cp -R ~/Library/Application\ Support/Claude/claude-code-sessions "$SCRATCH/Library/Application Support/Claude/"
cp ~/Library/Application\ Support/Claude/config.json "$SCRATCH/Library/Application Support/Claude/" # enables the account cross-check
# Copy only the pin list (jq ships with macOS 15+): the full claude_desktop_config.json can hold MCP server secrets.
jq '{preferences:{epitaxyPrefs:{"starred-local-code-sessions":.preferences.epitaxyPrefs["starred-local-code-sessions"]}}}' \
~/Library/Application\ Support/Claude/claude_desktop_config.json > "$SCRATCH/Library/Application Support/Claude/claude_desktop_config.json"
cp ~/.claude.json "$SCRATCH/"
claude-migrate plan --home "$SCRATCH" --include-orphans
claude-migrate apply --home "$SCRATCH" --from <old-account>/<old-org> --include-orphans
claude-migrate undo --home "$SCRATCH"
The Desktop check is skipped for a home that is not yours, and nothing outside the scratch directory is touched. ~/.claude/projects need not be copied; --include-orphans stands in for the transcripts you left out. Delete the scratch directory when done: the copy of ~/.claude.json holds your account details. --home is a testing flag hidden from claude-migrate help.
Other community projects that touch the same problem space (none is affiliated with this one). Descriptions summarize each project's own README as of October 2026:
| Tool | What it does |
|---|---|
| claude-code-export-import | Exports and imports Claude Code sessions between machines, accounts and app builds; rewrites transcripts. |
| transfer-claude-sessions | Copies Desktop sessions to a new account with dry run, backup and undo; Windows PowerShell. |
| claude-session-migrate | Claude Code skill and script that copies Desktop sidebar records to the new account; Windows-focused, backs up the sessions folder first. |
| claude-account-migration | Write-up of where Desktop keeps sessions and why a switch hides them, with helper scripts; macOS and Windows. |
| cowork-migrate | Moves Cowork sessions between Macs. |
| claude-folder-move | Retargets Claude Code sessions when a project folder moves. |
make check # test (race), vet, gofmt check, staticcheck
make build # bin/claude-migrate
make snapshot # local GoReleaser build, nothing published
See CONTRIBUTING.md for the TDD workflow and fixture rules. To add a regression fixture from your own store without leaking data, use go run ./scripts/anonymize -src <store dir> -out <dir> and check the output with the privacy grep described in CONTRIBUTING.md.
Contributions are welcome; read CONTRIBUTING.md first. Report vulnerabilities privately as described in SECURITY.md, not in a public issue. Release notes are in CHANGELOG.md.