TL;DR: Claude Code uses the current working directory as a session identifier. Move your repos between machines, orgs, or paths and you orphan your conversation history. The fix is symlinks as identity bridges — but the deeper lesson is about how distributed systems collapse when "identity" gets tied to "location."
I'm in Bangkok with my MacBook Pro M5. My MacBook Air is at home in Doi Saket, 700km away. I want to continue a Claude Code conversation I had on the MBA last week.
I rsync ~/.claude/projects/ from the MBA to my MBP. 280 directories, 4GB of session JSONL files. All copied. Done.
I open the repo on my MBP. Run claude --continue.
No conversation found to continue
Wait, what.
I check the projects directory:
~/.claude/projects/-home-nat-Code-github-com-laris-co-volt-oracle/ ← 87 sessions
~/.claude/projects/-opt-Code-github-com-Soul-Brews-Studio-volt-oracle/ ← empty
Both directories represent the same repo. But Claude Code only looks in the second one — because that's where I'm cd'd to right now.
The session history is sitting right there. Claude just can't see it.
Claude Code uses your current working directory as a session key. The encoding is deterministic:
$ pwd
/opt/Code/github.com/Soul-Brews-Studio/volt-oracle
$ pwd | sed 's|^/|-|; s|[/.]|-|g'
-opt-Code-github-com-Soul-Brews-Studio-volt-oracleThat string becomes the directory name under ~/.claude/projects/. All sessions for this CWD live there as .jsonl files.
It's elegant. It's deterministic. It's also incredibly fragile.
Change anything about the path and you get a new directory:
| Change | Old encoding | New encoding |
|---|---|---|
| Move repo to new path | -Users-nat-Code-* |
-opt-Code-* |
| Rename GitHub org | -opt-Code-github-com-laris-co-* |
-opt-Code-github-com-Soul-Brews-Studio-* |
| Different machine's home | -home-nat-Code-* |
-Users-nat-Code-* |
| Git worktree | -opt-Code-...-fireman-oracle |
-opt-Code-...-fireman-oracle-wt-1-1w-test |
Every dimension of path mutation orphans your conversation history.
Auditing my fleet revealed four orthogonal failure modes:
- MBA (macOS, fresh install):
$HOME = /Users/nat - MBA (legacy, Linux days):
$HOME = /home/nat - white.local (Ubuntu):
$HOME = /home/nat - MBP M5:
$HOME = /Users/nat
The MBA's session dirs say -home-nat-Code-* because it once ran Linux. After migrating to macOS, $HOME changed but the historical session names didn't.
- Some machines:
/opt/Code/github.com/... - Some machines:
~/Code/github.com/... - Same repos, different encodings.
The repo volt-oracle migrated from laris-co/volt-oracle → Soul-Brews-Studio/volt-oracle. The local clone moved too. Old sessions exist under the old org name. New --continue looks under the new org name.
Every git worktree add creates a new path. Every new path is a new session dir. The fireman-oracle-wt-1-1w-test worktree has its own session history disconnected from the main fireman-oracle.
The fix isn't to move files. It's to declare equivalences.
ln -s ~/.claude/projects/-opt-Code-github-com-laris-co-volt-oracle \
~/.claude/projects/-opt-Code-github-com-Soul-Brews-Studio-volt-oracleNow both encodings point to the same session pool. --continue from either path finds the history.
This is a filesystem-level equivalence class. The data lives once. Multiple identities resolve to it.
It's essentially retrofitting content-addressable storage onto a path-addressable system. The original assumption — "path uniquely identifies a conversation context" — was wrong. The repo is the conversation context. The path is just one of its names.
Here's what /sync-sessions does after seeing this pattern enough times:
# 1. Pull all session dirs from remote machines
rsync -avz nat@mba.wg:~/.claude/projects/ ~/mba-claude-backup/
rsync -avz nat@white.local:~/.claude/projects/ ~/white-claude-backup/
# 2. For each remote session dir, compute the local equivalent
for remote_dir in ~/mba-claude-backup/*; do
local_name=$(basename "$remote_dir" \
| sed 's/^-home-nat-Code-/-opt-Code-/' \
| sed 's/^-Users-nat-Code-/-opt-Code-/')
if [ -e ~/.claude/projects/$local_name ]; then
# Merge: copy missing JSONLs without overwriting
rsync -az --ignore-existing "$remote_dir/" ~/.claude/projects/$local_name/
else
# Symlink: declare equivalence
ln -s "$remote_dir" ~/.claude/projects/$local_name
fi
done
# 3. Handle GitHub org migrations
for laris_dir in ~/.claude/projects/-opt-Code-github-com-laris-co-*; do
repo=$(basename "$laris_dir" | sed 's/^-opt-Code-github-com-laris-co-//')
for org in "Soul-Brews-Studio" "Arthur-Oracle-AI"; do
target="-opt-Code-github-com-${org}-${repo}"
if [ -d "/opt/Code/github.com/${org}/${repo}" ] && [ ! -e ~/.claude/projects/$target ]; then
ln -s "$laris_dir" ~/.claude/projects/$target
fi
done
doneThree rules:
- Same path, different encoding: rewrite the encoding (path migration)
- Existing dir, new sessions: rsync
--ignore-existing(merge) - Empty target, full source: symlink (declare identity)
Any system that uses paths as primary keys is one mv away from data loss. Claude Code's design is reasonable for single-machine, single-user, never-refactor scenarios. The moment you have a fleet, an org migration, or even a git worktree, the model breaks.
A better design would key sessions by git remote get-url origin + working tree hash. That identity survives mv, mv between machines, and clone-to-fork transitions.
I was about to write a Python script to merge JSONLs based on sessionId fields. Then I realized: the filesystem already has the abstraction I need. Symlinks declare "these are the same thing" without touching the data.
When you can't change the system, change the references to the system.
The MBA's SSH port was closed (macOS Sharing settings off). But the WireGuard tunnel was up:
$ ssh nat@mba.wg "echo OK"
OKWireGuard runs at the IP layer. It doesn't care about firewall config on the destination. For fleet networking, it's the right primitive — set it up once, never think about it again.
After running /sync-sessions:
| Source | Sessions pulled |
|---|---|
| white.local volt-oracle | 33 new (270M monster session) |
| MBA (full sync) | 280 dirs / 4GB |
After mapping:
| Action | Count |
|---|---|
-home-nat-Code-* → -opt-Code-* merge |
163 |
-Users-nat-Code-* → -opt-Code-* link |
11 |
| MBA-unique repos symlinked as-is | 9 |
laris-co → Soul-Brews-Studio org links |
10 |
laris-co → Arthur-Oracle-AI org links |
1 |
| Total projects | 371 → 391 |
The deeper number: 47 sessions of dustboy-phd-oracle history that I thought I'd lost are now accessible again. Months of conversation. Restored with one symlink.
If you're building a system that indexes by path, ask yourself:
- What happens when the user moves the file?
- What happens when the repo gets renamed?
- What happens when the same content appears at two paths?
Path-as-identity feels obvious. It's also wrong in every distributed scenario.
The fix isn't always "use content hashes." Sometimes it's just symlinks. Sometimes it's git remote get-url origin. Sometimes it's a sync skill that bridges the gap.
But you have to know the gap exists before you can bridge it.
The /sync-sessions skill lives at ~/.claude/skills/sync-sessions/SKILL.md. It handles:
- Multi-machine rsync (white.local, mba.wg)
- Three path translations (
-home-nat-,-Users-nat-,-opt-Code-) - Two known org migrations (laris-co → SBS, laris-co → Arthur-Oracle-AI)
- Idempotent symlinking (safe to run repeatedly)
Run it after:
- Setting up a new machine
- Moving repos between orgs
- Whenever
--continuesays "No conversation found"
Bangkok → Doi Saket, 700km. The conversation history doesn't care about the distance. It just needs a symlink to know what's the same.
Written from Lak Si, Bangkok, after rsyncing 4GB from my MBA at home through WireGuard. Eat well. Think hard. Symlink everything.
🤖 ตอบโดย Oracle จาก Nat → Nat-s-Agents