claude-memory-tool
v0.2.1
Published
Sync a project's Claude Code memory and session history across machines via a private git repo, without committing anything to the project.
Maintainers
Readme
claude-memory-tool
Move between machines without Claude Code losing the thread.
Claude Code keeps a project's memory and conversation transcripts in
~/.claude/projects/<path-slug>/, outside the repo. A git clone on a second
machine brings the code and nothing else, so you start cold. This tool carries
memory and session history across, through a second private git repo you own —
so machine B picks up exactly where machine A stopped, --resume included.
Your project repo is never touched. Not one file, not one line in
.gitignore. Teammates cloning the project see no trace of any of this.
Install
npx claude-memory-tool installDrops the skill into .claude/skills/memory-sync/ and four commands into
.claude/commands/.
Use
First machine
/memory-setupIt reads your project's origin, asks for a separate private repo to store
memory in, and mints a 64-hex id. It then prints three values:
storage repo: [email protected]:you/claude-memories.git
project repo: [email protected]:acme/checkout-api.git
project id: 7a265062958de42191e053264405d1c60bac2d588ae6961b4856c4b9eedabea8Save them. Nothing about this link is committed anywhere, so a second machine
cannot discover them — you type them in there. They also live in
claude-memory-tool/setup.md on this machine, which is local and gitignored.
Then, before you switch machines:
/memory-sync-uploadOther machines
Clone the project, install, and join:
/setup-second-memoryIt asks for the three values, checks the id actually exists in the storage repo, and shows you what is behind it before saving anything:
✓ Found "checkout-api"
last upload: a3f91c04 by DESKTOP-OFFICE at 2026-08-20T09:12:44Z
contents: 14 memory file(s), 3 session(s)That summary is your confirmation the id was right. Then:
/memory-sync-downloadStart Claude Code in that directory and --resume. The conversation from the
first machine is there.
How it fits together
Machine A Storage repo (private) Machine B
───────── ────────────────────── ─────────
/memory-setup
→ mints id, saves it locally, commits nothing
/memory-sync-upload ─────────► <64-hex>/ │
manifest.json │
sync-log.md │
memory/ │
sessions/ │
git clone the project
(no trace of the tool)
npx claude-memory-tool install
/setup-second-memory
← you type the 3 values
◄──────────────────── /memory-sync-download
claude --resumeTwo repos, and they never mix:
- Project repo — your code. Carries nothing belonging to this tool.
- Storage repo — private, one per user, holds memory and transcripts for
every project under one opaque
<64-hex>/folder each. The folder name reveals nothing about the project.
Read this before you set it up
Transcripts contain everything. Every prompt, every file excerpt Claude read, every command's output — including any secret that ever crossed the terminal. Setup refuses a public storage repo, and that refusal is the point, not a formality.
Tracking who has what
Every upload gets a short random ref like a3f91c04. It travels through the
manifest, the storage repo's commit message, and both logs — so "which upload
does this machine have?" has one exact answer instead of being inferred from
clocks that machines disagree about.
Shared log — <project_id>/sync-log.md in the storage repo. Uploads only:
| ref | uploaded (UTC) | machine | memory | sessions | had |
|---|---|---|---|---|---|
| `a3f91c04` | 2026-08-20 09:12Z | MACBOOK | 15 | 3 | `7b2e0f11` |
| `7b2e0f11` | 2026-08-19 17:40Z | DESKTOP-OFFICE | 14 | 3 | — |The had column is what that machine had downloaded when it uploaded — so
download history shows up here too, without downloads ever having to write.
Downloads are deliberately not recorded here. Writing on download would turn a read into a push, which fails when two machines pull at once, and would add a commit every time you pull.
Local log — claude-memory-tool/machine-log.md, one per machine, never
pushed. Both actions, with the details that only matter to you:
| when (UTC) | action | ref | detail |
|---|---|---|---|
| 2026-08-20 09:12Z | upload | `a3f91c04` | 15 memory files, 3 transcript(s) |
| 2026-08-20 08:55Z | download | `7b2e0f11` | 14 memory files, 3 transcript(s) from DESKTOP-OFFICE; backup at slug.backup-20260820-085502 |Because refs are exact, upload can tell you precisely where you stand:
✗ This machine is 2 upload(s) behind the storage repo.
you have: 7b2e0f11
landed since: a3f91c04, 55d10e83
most recent: MACBOOK at 2026-08-20T09:12:44ZDesign notes
Nothing is committed to the project repo. claude-memory-tool/ contains a
.gitignore holding just *, so the directory ignores its own contents
including that file. No entry in the project's committed .gitignore, nothing to
explain to teammates, and the storage repo URL and project id never leave your
machine. The cost is that a second machine cannot discover the link — hence
/setup-second-memory.
Paths are rewritten, and they have to be. The state directory is named after
the project's absolute path, so it differs per machine, and every transcript line
embeds a cwd. Stored transcripts hold a portable
__CLAUDE_MEMORY_SYNC_ROOT__ placeholder instead; upload folds the local root
into it, download expands it to whatever the local root is. Any number of
machines with different paths agree on the stored bytes, which also keeps git
diffs free of path churn.
Only the top-level cwd field is touched. Absolute paths inside historical tool
results are left alone — they are a record of what happened on another machine,
and blind find-and-replace over captured text corrupts more than it fixes.
Download always backs up first, to
~/.claude/projects/<slug>.backup-<timestamp>/.
Upload refuses to clobber a newer upload from another machine. Download
first, or pass --force if you are certain this machine holds the state to keep.
A machine is never told it is behind its own upload.
Sessions merge by length, not by timestamp. Transcripts are append-only, so
"did this machine carry the session further?" is a prefix test. If two machines
extended the same session differently, both copies are kept
(<uuid>.conflict-<machine>.jsonl) and you are told.
Transcripts older than max_session_age_days (default 30) are not uploaded,
and the command says which ones it skipped. Memory files are always uploaded in
full.
Line endings are pinned (core.autocrlf=false, plus * -text in the storage
repo). Transcripts are compared byte-for-byte, so a Windows checkout rewriting LF
to CRLF would turn every cross-platform comparison into a false conflict.
Transport
Plain git against a cached clone under ~/.claude/mem-sync/, using whatever
credentials the machine already has — SSH key, credential helper, gh/glab
login. No tokens are stored anywhere. GitHub, GitLab, and self-hosted GitLab all
take the same code path.
Files on your machine
<project>/
.claude/skills/memory-sync/ the skill
.claude/commands/ the four commands
claude-memory-tool/ all gitignored, by a .gitignore of "*"
setup.md config: the two repos, the id
state.local.json what this machine last uploaded/downloaded
machine-log.md this machine's readable historyCommands and flags
| | |
|---|---|
| /memory-setup | --storage-repo <url> --max-session-age-days <n> --force --allow-public |
| /setup-second-memory | --storage-repo <url> --project-repo <url> --project-id <64 hex> --force --allow-public |
| /memory-sync-upload | --force |
| /memory-sync-download | --dry-run |
CMS_MACHINE_NAME overrides the hostname used to attribute uploads, which is
useful when several checkouts share one host.
Tests
npm testSimulates two machines with different home directories and different project
paths against local bare repos, and exercises the whole cycle: setup refusals,
that the project repo stays untouched, joining by id (including malformed ids,
wrong ids, and wrong project repos), cross-machine download with path rewriting,
both logs, the ref-based divergence guard, session forks, the age cutoff, and
--force.
License
MIT
