Skip to content

fix(cloud): remap project paths across machines via git remote#204

Merged
vakovalskii merged 1 commit intomainfrom
fix/cloud-path-remap
Apr 21, 2026
Merged

fix(cloud): remap project paths across machines via git remote#204
vakovalskii merged 1 commit intomainfrom
fix/cloud-path-remap

Conversation

@vakovalskii
Copy link
Copy Markdown
Owner

Closes #154.

Problem

Cloud Sync serialized a session with the source machine's absolute project path (e.g. /Users/alice/work/app) and, on pull, derived the Claude/Cursor project key by sanitizing that path. So a session pushed from macOS landed on a Linux machine under a project bucket matching the mac path — even if the same repo was actually checked out at /home/bob/src/app locally. Imported sessions appeared under a logical project that didn't exist on the destination.

Fix

Identity-by-git-remote, with a safe fallback ladder:

On push (serializeSession):

  • getProjectGitInfo(session.project) → capture remote.origin.url and gitRoot
  • Normalize the remote URL (https://…, git@…, ssh://…, credentials, trailing /, .git, case → single canonical form like github.com/user/repo)
  • Add gitRemote and gitRoot to the canonical payload
  • Bump canonical version: 1 → 2

On pull (deserializeSession, Claude + Cursor branches):

  1. If gitRemote is set and there's a local project whose remote matches → write into that project's key. Session shows up under the right local path.
  2. Else if gitRemote is set but no local checkout matches → write into a neutral bucket -cloud-import-{slug}/. Imports don't pollute a nonexistent source-machine path bucket.
  3. Else (legacy v1 payload or session with no git) → preserve existing behavior.

Local-project lookup is cached for 30s.

Scope / non-goals

  • Claude + Cursor restore only (the two agents with path-keyed storage on disk)
  • Codex/OpenCode/Kiro aren't affected (Codex is date-keyed; OpenCode/Kiro go to a neutral import dir already)
  • No migration needed — v1 payloads still deserialize the old way
  • No API/transport/encryption changes

Tests

New test/cloud-remote-normalize.test.js covers normalizeGitRemote (https/ssh/git@/credentials/case/trailing slash/.git) and slugifyRemoteForDir. Run: node --test test/*.test.js.

Test plan

  • Push a Claude session from machine A (/Users/alice/work/app) to cloud
  • Pull on machine B where repo is at /home/bob/src/app — session appears under the bob path project
  • Pull on machine C with no local checkout — session appears in ~/.claude/projects/-cloud-import-github-com-…/
  • Pull a legacy v1 payload (push before this PR landed) — old behavior preserved
  • Same scenarios for a Cursor session

Cloud Sync previously preserved the source machine's absolute project
path and derived the Claude/Cursor project key from it on pull. That
tied project identity to the source machine, so sessions pushed from
e.g. /Users/alice/work/app on macOS were restored under a Claude
project bucket that did not exist on a Linux machine where the same
repo lived at /home/bob/src/app.

Changes:
- serialize: capture normalized git remote URL + git root alongside
  the source path; bump canonical version 1 → 2
- deserialize (claude + cursor):
  1. If a local checkout exists whose git remote matches, write into
     that project's key (session shows up under the right project)
  2. Else if gitRemote is known, write into a neutral bucket
     -cloud-import-{slug}/ so imports don't pollute nonexistent
     source-machine path buckets
  3. Else (legacy v1 payload or no git info) preserve prior behavior
- normalizeGitRemote: https/ssh/git@ variants + case + credentials +
  trailing slashes/.git all map to one canonical form
- findLocalProjectByRemote: 30s-cached scan of loaded sessions
- Add unit tests for normalization helpers
Copy link
Copy Markdown
Collaborator

@NovakPAai NovakPAai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Logic is sound, tests cover all cases — ready to merge.

A few observations:

⚠️ Cold cache in findLocalProjectByRemote can be expensive
On first call it iterates all sessions via loadSessions() + getProjectGitInfo() per session. On 300+ sessions this is noticeable. The 30s cache helps on repeat calls, but the first pull after a dashboard restart will be slow. Consider building the map over projects only (not sessions), or logging a warning when the scan takes >1s.

ℹ️ require('./data') inside a function
Lazy require works but is non-idiomatic — usually a sign of circular dependency. Worth a one-line comment explaining why, so a future reader doesn't move it to the top and introduce a cycle.

ℹ️ Silent catch {} blocks
Understood that this is by design (don't break the main flow), but failures in git remote resolution will be hard to diagnose. Even catch (e) { /* git info unavailable */ } or a debug-level log would help.

✅ What's good:

  • normalizeGitRemote correctly handles all URL variants (https/ssh/git@, embedded credentials, .git suffix, case)
  • Fallback ladder (matched local → neutral bucket → legacy behavior) is safe and well-thought-out
  • Payload versioning v1→v2 with backward compatibility is the right approach
  • No conflicts with other open PRs

@vakovalskii vakovalskii merged commit 3fd4943 into main Apr 21, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cloud Sync: Claude project path is not remapped across machines

2 participants