Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save nazt/dc4bab05a7b0b06fcffd3c4003e18223 to your computer and use it in GitHub Desktop.

Select an option

Save nazt/dc4bab05a7b0b06fcffd3c4003e18223 to your computer and use it in GitHub Desktop.
Path-as-Identity Considered Harmful: Cross-Machine Session Sync in Claude Code

Path-as-Identity Considered Harmful: Cross-Machine Session Sync in Claude Code

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."


The Day My Sessions Disappeared

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.


How Claude Code Maps Directories to Sessions

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-oracle

That 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.


Four Collision Dimensions

Auditing my fleet revealed four orthogonal failure modes:

1. Home Directory Divergence

  • 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.

2. Code Root Convention

  • Some machines: /opt/Code/github.com/...
  • Some machines: ~/Code/github.com/...
  • Same repos, different encodings.

3. Org Migration on GitHub

The repo volt-oracle migrated from laris-co/volt-oracleSoul-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.

4. Worktrees

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 Symbolic Link as Identity Bridge

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-oracle

Now 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.


The Sync Algorithm

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
done

Three 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)

What I Learned

1. Path-as-Identity Is a Time Bomb

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.

2. Symlinks Are the Cheapest Migration Layer

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.

3. WireGuard Beats SSH for Fleet Work

The MBA's SSH port was closed (macOS Sharing settings off). But the WireGuard tunnel was up:

$ ssh nat@mba.wg "echo OK"
OK

WireGuard 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.

4. Real Numbers

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-coSoul-Brews-Studio org links 10
laris-coArthur-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.


The Pattern: Identity ≠ Location

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.


Code

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 --continue says "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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment