You rename a project folder. You open Claude Code. Your history is gone — or rather, it is not gone, it has simply detached itself and is now sitting in a second, dead project beside the real one.
Claude Code does not follow a renamed directory. Sessions stay attached to the path they were created under. That much is easy to work out. What is less obvious, and what makes this genuinely annoying to fix, is that your project's identity is recorded in four independent places, and fixing three of them looks exactly like fixing none of them.
Here is what I found while cleaning this up properly.
Scope and shelf life. Everything below was verified on 2026-08-14 against Claude Code 2.1.223 on macOS 26.6.1. None of it is documented by Anthropic, which means it is all implementation detail and can change without warning. Treat this as a field report, not a specification. Check before you trust it.
The four stores
| Store | Location | Holds |
|---|---|---|
| CLI transcripts | ~/.claude/projects/<encoded-path>/ |
<sessionId>.jsonl, plus subagents/, tool-results/, memory/ |
| Desktop sidebar index | ~/Library/Application Support/Claude/claude-code-sessions/<id>/<id>/local_*.json |
one flat JSON per session — this is what the sidebar renders |
| Global config | ~/.claude.json → projects map |
trust, allowed tools, onboarding state |
| Prompt history | ~/.claude/history.jsonl |
past prompts, tagged by project |
They are not views of one thing. They are four separate records that happen to agree most of the time. On my machine right now the CLI store knows about 14 project paths and the desktop index knows about 8 — six projects exist as transcripts with no sidebar entry at all. That divergence is normal, and it is the reason a partial fix is so convincing and so wrong.
The one that bites: the desktop index
If you go looking for the sidebar's data, you will naturally reach for ~/.claude/projects/. It has directories named after your projects. It is obviously the project store.
It is not what the sidebar renders.
The sidebar reads a separate pile of JSON under ~/Library/Application Support/Claude/. On my machine that is 656 files, all at the same nesting depth, in directories named with opaque ids rather than paths. 655 of them are sessions, each carrying cwd and originCwd fields; the odd one out is a scheduled-tasks.json that simply shares the directory and has neither field. Anything reading this store should expect files that are not sessions. The sidebar groups by that inner field, not by folder. Nothing about the directory layout tells you it is a per-project store, because it isn't one.
Two consequences:
- If you clean up
~/.claude/projects/and stop there, the old project reappears in the sidebar after every restart. Your fix looks like it silently failed. - The desktop app reads this index at launch. Until you quit and reopen Claude, nothing you change will show up. If you are helping someone else through this, say so explicitly, or they will reasonably report that nothing happened.
The encoding is lossy
The CLI directory name is the absolute path with every non-alphanumeric character replaced by -:
/Users/jo/Projects/MyApp → -Users-jo-Projects-MyApp
I checked this against the project directories on my machine: of 15, the 14 that record a readable cwd reproduce exactly under re.sub(r'[^A-Za-z0-9]', '-', cwd). The fifteenth has no transcript carrying a cwd, so there is nothing to check it against — which turns out to matter later. So far, so simple.
The trap is that both / and . collapse to -, and the mapping is therefore not reversible:
>>> enc("/x/foo.com")
'-x-foo-com'
>>> enc("/x/foo/com")
'-x-foo-com'
A sibling project called foo.com encodes identically to a child directory foo/com. If you are matching project directories by name prefix — which is the obvious way to find "this project and everything under it" — you will quietly scoop up an unrelated project that merely happens to have a dot where the other has a slash.
You cannot fix this by being cleverer about the name, because the information genuinely is not in the name. The only reliable move is to open the transcripts and read the cwd they actually recorded, and treat the name as a hint rather than an answer.
And if a directory records nothing — no transcripts, or transcripts with no cwd — the correct answer is to leave it alone. "No evidence" is not the same as "yes." That distinction is worth more than it sounds: it is the difference between skipping a directory and deleting someone else's project.
The case-insensitivity trap
This one is the reason I am writing any of this down, because the naive fix destroys data.
macOS volumes are case-insensitive by default. So if you rename myapp to MyApp, then:
-Users-jo-Projects-myapp
-Users-jo-Projects-MyApp
are not two directories. They are one directory with two spellings. Move files from the first to the second and you are moving them onto themselves. Then, if you follow up by deleting "the old directory" — as any sane migration would — you delete the only copy.
The rule that falls out of this: compare directories with os.path.samefile(), never with string equality. String comparison says these are different; the filesystem says they are the same, and the filesystem is right. Every decision downstream — is this a move, or a rename? is it safe to delete the source? — depends on getting that one comparison right.
What you do not need is a workaround for the rename itself. I had assumed a case-only rename would be a no-op on a case-folding volume, and that changing myapp to MyApp therefore required two hops through a temporary name. That is not true here. Tested on three case-folding volumes on this machine — including the one holding ~/.claude — a direct os.rename('myapp', 'MyApp') changes the on-disk spelling and preserves the contents:
/private/tmp: case-folding=True | after rename dir=['MyApp'] payload_ok=True
home (~): case-folding=True | after rename dir=['MyApp'] payload_ok=True
~/.claude: case-folding=True | after rename dir=['MyApp'] payload_ok=True
Files behave the same way. I cannot speak for HFS+ or older releases, where the two-hop folklore may well have come from something real.
This is worth dwelling on, because the two-hop is not merely unnecessary — it is actively worse. If your process dies between the two renames, the entire project sits under a temporary name that Claude Code will not read and that a prefix scan will not find, so a re-run reports "nothing found" and the data looks lost. A defensive workaround for a problem you never had can introduce the failure mode you were afraid of. Check the assumption before you engineer around it.
There is a broader lesson buried in this one. If your development machines are macOS and your CI is Linux, any test you write for case-handling behaviour is testing something different in the two places. On a case-sensitive filesystem those two directory names really are two directories, so the entire code path above never executes. A test can pass on CI while protecting nothing at all. If it matters, drive the logic directly instead of relying on filesystem semantics to set the scene for you.
Cloud sessions are a different animal
Sessions created in Claude Code on the web are not stored locally at all. They live on the server and are grouped by the repository name captured when the session was created.
The symptom is a sidebar group whose label matches nothing on your disk. No transcripts, no config entry, nothing.
And here is the part that catches people: renaming the GitHub repo does not relabel existing cloud sessions. The name is a snapshot taken at creation. Renaming gives you a second group — old sessions under the old name, new ones under the new — which is precisely the fragmentation you were trying to eliminate. I have now watched this happen twice, on two different repos, with the same result both times.
Local surgery cannot help, because there is nothing local to operate on. The realistic options are to archive the stale sessions (they stay recoverable and the group disappears), or move them into a custom group, which is also the only way to get a label containing spaces, since repo names cannot have them.
If you are doing this by hand
A short checklist, in the order that avoids the traps:
- Find out what each sidebar group actually is before touching anything. A group can be a live project, a git worktree (any session started in a worktree becomes its own project — this is by design, not corruption), a path that no longer exists, or a cloud session. Sidebar labels are not paths and will mislead you.
- Confirm directory ownership by reading
cwdfrom the transcripts, not by matching the encoded name. - Back up all four stores first. Whatever you are about to do, you are doing it to your own conversation history.
- Handle the four stores together. Three out of four reads as failure.
- Quit and restart the desktop app. Nothing visibly changes until you do.
One last thing, which is more of a principle than a step: when you rewrite these files, only touch the structural cwd field. Do not run a find-and-replace over the message bodies. Some of those conversations discuss the old path, and rewriting them falsifies your own historical record. After a correct migration, grepping for the old name will still return hits. That is the right outcome, even though it looks like the job is unfinished.

