reunite
sessions answers the moment you sign in with your other account and the sidebar is suddenly almost empty. Nothing was deleted. Claude's desktop app keeps a separate conversation index for each account it has been signed in as, and it shows you only the one belonging to whoever is signed in now. This unions those indexes, so whichever account you use, you see the whole history.
Each optional.
- [--apply]
- [--into=all|current|<accountUuid>]
- [--no-titles]
- [--undo]
The prompts CI scores this skill against, on every release.
Fires on
- I signed in with my other Claude account and now the sidebar is almost empty. Where did all my conversations go?
- 换了个账号登录,session 全没了
- Can my two accounts on this machine share the same conversation list?
- I renamed a bunch of conversations under one account but my other account still shows the old titles.
Stays out of
- Rename these conversations onto the dated scheme.
- Move my Claude history over to my new laptop.
Nothing was lost, and you can prove it
Two stores hold a conversation, and only one of them has ever heard of accounts.
The conversation itself — every message, every tool call — is a JSONL file under ~/.claude/projects/, filed by the directory you were working in. Open one and there is no account field anywhere in it. That is why claude --resume in a terminal has been listing all of your conversations the whole time, no matter which account is signed in.
What the sidebar reads is a second, much smaller set of files: one index entry per conversation, stored under a path that begins with your account's identifier. Sign in as a different account and the app reads a different directory. The conversations are still on disk, in full, untouched.
So the fix is much smaller than the symptom. Nothing needs recovering; the lists need merging.
What it does
It copies each account's index entries into every other account's index.
Session index ~/Library/Application Support/Claude/claude-code-sessions
account bc95701b… 36 conversations lands in ee9e5ec5…
account cb24d9c9… 358 conversations lands in c04cc789…
account d58bde8d… 81 conversations lands in 2618534f… <- signed in
Plan: 720 entries to copy into 3 account index(es), +114.3MB
skipped 224 whose transcript is goneThat report is the whole of a default run. It writes nothing until you pass --apply, because 114MB and three directories is not a decision to make on someone's behalf without showing them the number first.
Renaming, and why one run is not enough
A rename writes one index file. Once a conversation exists in three indexes, renaming it under one account leaves the other two holding the old name — and copying cannot fix that, because the entry is already there.
So each run also reconciles titles. Where copies of one conversation disagree, the most recently written file wins and its name is written into the others. Only the title moves; everything else in those files is that account's own record of the conversation.
The report lists every one it is about to change, because the signal is not infallible — file mtime is the only timestamp a rename actually moves, and an unrelated rewrite of a stale copy can make it the newest. --no-titles turns the pass off entirely.
This is what makes a naming sweep worth running at all: rename under one account, run this, and the names reach the others.
What it refuses to do
It never deletes. Every run only adds files, and it records each one it added. --undo removes exactly those paths — not files the app wrote, not entries a previous merge already reconciled.
It skips entries whose conversation is gone. An index entry can outlive its transcript. Copied around, it becomes a row in your sidebar that opens to nothing, which is worse than not being there. Those are counted in the report and left behind unless you ask for them.
It runs again cleanly. New conversations only land in the index of the account you were signed in as, so this is a thing you re-run, not a thing you do once. A second run with nothing to do plans zero copies and says so.
The restart
A merge does not appear until the desktop app restarts. The app reads this index when it starts and does not look at the directory again while it is running.
That matters more than it sounds, because restarting interrupts whatever conversations are still running. The run tells you to restart rather than doing anything about it, and checking what is live first is worth the ten seconds.
The instructions the agent executes
SKILL.md in full — what Claude Code reads when this skill fires. The page above explains it; this is the thing itself.
# Reunite
Union the desktop app's per-account conversation indexes, so whichever account is signed in sees the whole history.
## What is actually lost
Nothing. Establish that before offering to fix anything, because the fix is much smaller than the symptom suggests.
Two stores hold a conversation, and only one of them knows about accounts:
| Store | Path | Account-aware |
|---|---|---|
| Transcript — the conversation itself | `~/.claude/projects/<slugged-cwd>/<cliSessionId>.jsonl` | **No.** The JSONL carries `cwd`, `sessionId`, `version`, `gitBranch` and no account field at all. |
| Index — what the sidebar lists | `~/Library/Application Support/Claude/claude-code-sessions/<accountUuid>/<orgUuid>/local_*.json` | **Yes.** Identity is the directory path; nothing inside the file names an account. |
So switching accounts hides conversations from the sidebar and deletes none of them. `claude --resume` in a terminal reads the transcript store directly and has been listing all of them the whole time — say so, because it is the answer for anyone who only needs to reach one old conversation.
## Run it
```bash
python3 scripts/merge.py # report only — what would be copied, and how much disk
python3 scripts/merge.py --apply # write
python3 scripts/merge.py --undo # remove exactly what --apply wrote
```
Report first, always. The report names each account index, its conversation count, and the org subdirectory a copy would land in. Read it out before writing: `--apply` on three accounts moved 114MB here, and someone who has not seen the number has not agreed to it.
`--into` narrows what receives the union — `all` (default), `current` for just the signed-in account, or a specific `accountUuid`. Narrow it when one of the accounts is long dead and does not deserve a copy of everything.
**"Signed in" means the desktop app, not the CLI.** They hold separate logins and are routinely on different accounts, so `~/.claude.json` answers a different question — it names the account `claude` authenticates as, not the one whose sidebar is on screen. The app records its own as `lastKnownAccountUuid` in `config.json` beside the index, and that is the index a rename actually lands in. Check it before concluding a rename did not work; it may have worked in the other account.
## Titles drift after a union, and a second run is where that shows
A rename writes one index file. So once a conversation exists in three indexes, renaming it under one account leaves the other two holding the old name, and copying cannot fix it — the entry is already there, so a plain union skips it.
Each run therefore also reconciles titles: where copies of one conversation disagree, the most recently written file wins, and its `title`, `titleSource` and `previousTitles` are written into the others. Nothing else in those files moves; the rest is that account's own record of the conversation.
File mtime is the signal because it is the only timestamp a rename actually moves — `lastActivityAt` records the conversation, not the record of it. That is not infallible: an unrelated rewrite of a stale copy makes it the newest, and it then wins with the older name. So every reconciliation is listed by name in the report, the value it replaced is recorded, and `--no-titles` turns the whole pass off.
This is what makes a naming sweep worth running. Rename under one account, run this, and the names reach the others.
## What the script refuses to do
- **It never deletes.** Every run only adds files, and records both what it added and every title it replaced in `.session-merge-manifest.json` beside the account directories. `--undo` removes exactly those paths and puts exactly those titles back — not files the app wrote, not titles this skill never touched. A manifest written before titles were reconciled is a bare list of paths and is still read as one, so an older merge stays undoable.
- **It skips orphans.** An index entry whose `cliSessionId` has no transcript under `~/.claude/projects/` would appear in the sidebar and open to nothing, which is worse than not appearing. The report counts them; `--keep-orphans` copies them anyway.
- **It is idempotent.** A second run with nothing new plans zero copies and zero reconciliations. Run it again after every stretch of work under one account, and after any renaming pass: new conversations only land in that account's index, and new names only land in that account's copy.
## The restart
**A merge does not show up until the desktop app restarts.** The app reads this index at startup and does not rescan the directory while running — verified by writing an entry with a current timestamp and watching it stay invisible to a running app.
So the last line of any report is the restart, and it is worth naming what the restart costs: running conversations are interrupted. Check what is live first — `list_sessions` on the session-management MCP shows which ones are still running — and let the user pick the moment.
## Reporting
```
Session index <root>
account <uuid> <N> conversations lands in <orgUuid> <- signed in
...
Plan: <N> entries to copy into <M> account index(es), +<size>
skipped <N> whose transcript is gone
<N> stale titles to reconcile
<account> → <winning title>
```
After `--apply`, say how many were copied and how many titles were reconciled, that `--undo` takes both back, and that the sidebar is unchanged until the app restarts. A merge reported as done while the sidebar still looks the same reads as a failure.
## Platform
The paths above are macOS. `CLAUDE_DESKTOP_SESSIONS_DIR` overrides the index root; the script exits with that hint rather than guessing when the directory is not there.Also in Dev
- sync/dev:syncsync does one thing: it lays out the difference between your local repository and its remote, and performs only the part of that which is unambiguously safe.
- ship/dev:shipship takes a change from your working tree to a merged pull request: branch off, run the project's own tests, commit, open the pull request, wait for CI, merge, then clean up the worktree.
- cleanup/dev:cleanupAfter a change ships, the leftovers stay: the local branch, its copy on the remote, the worktree it was built in, and sometimes a directory holding nothing but ignored files. cleanup removes those, and keeps and reports anything whose history it cannot account for.
- retitle/dev:retitleA conversation list gets hard to use not because the names are ugly but because they carry nothing you can sort on.
- steward/dev:stewardsync, ship and cleanup each work in the repository you are standing in. steward is the one that runs that loop across every repository you have actually been working in — it finds them, sweeps them, and hands back a single report of what is waiting on you: which branches are ready to merge, which pull requests are blocked and on what, which branches were written and never opened, and which worktrees a live session is still sitting in.
- handoff/dev:handoffhandoff makes a conversation you are having in one agent appear in the other one's history while it is still going.