Two Machines, One Memory
How to keep a Claude Code memory store in lockstep across two machines, using nothing but git and two hooks.
We've been running Claude Code against our Optimizely CMS 12 / Commerce Connect 14 platform for a while now, and one thing surprised me pretty quickly: it gets good at our codebase, not just at coding.
It remembers that a migration has to run before a specific InitializableModule or things break in strange ways. It remembers feature flags nobody has cleaned up yet. It remembers that the last DXP copydown wiped out a SiteDefinition nobody noticed until hours later.
Then I'd switch to my other machine, start a new session — and all of that was gone. Same agent, same codebase, back to square one.
The problem
Claude Code's memory is just files on disk. On one machine that works well — it accumulates and the agent gets more useful over time. On two machines, you get two memories, both growing, both changing, neither aware the other exists. That's worse than no memory: at least with nothing, you know where you stand.
Make the memory folder a repo
The fix isn't clever. Turn the memory folder into its own git repository, and sync it the same way you'd sync any shared state.
# from the memory folder for your project
cd ~/.claude-work/projects/<project-key>/memory
git init
git remote add origin git@github.com:you/claude-memory.git
git add -A && git commit -m "Initial memory store"
git push -u origin main
I did the same for plans/, where the agent writes design notes before larger changes, kept in a repo outside any application repo so it survives branch cleanups and repo resets.
Pull on start, push on end
Claude Code hooks fire on session lifecycle events. A SessionStart hook runs before the session begins; a SessionEnd hook runs after it finishes.
{
"hooks": {
"SessionStart": [{
"hooks": [{ "type": "command",
"command": "pwsh -File ~/.claude-work/hooks/memory-sync-pull.ps1" }]
}],
"SessionEnd": [{
"hooks": [{ "type": "command",
"command": "pwsh -File ~/.claude-work/hooks/memory-sync-push.ps1" }]
}]
}
}
The naive first version of the pull hook:
cd $env:MEMORY_REPO
git pull --rebase --autostash --quiet 2>$null | Out-Null
exit 0
And the push hook:
cd $env:MEMORY_REPO
git add -A
git commit -m "session $(Get-Date -Format o)" --quiet 2>$null
git push --quiet 2>$null
That's the entire mechanism: pull at start, commit and push at end. No manual step, nothing to remember to do.
Hardening the pull
The v1 pull hook swallows every error and always exits 0, on purpose — I didn't want a flaky git pull to block a coding session. The cost is that a failed rebase (a conflict, a detached HEAD) goes completely silent, and every session after that keeps committing memory onto a head that will never push cleanly. I found this a month and sixty-four orphaned commits later.
Two changes close that gap: check repository state after the pull, not just its exit code, and make failures persistent instead of one-shot.
cd $env:MEMORY_REPO
git pull --rebase --autostash --quiet 2>$null | Out-Null
$rebasing = (Test-Path ".git/rebase-merge") -or (Test-Path ".git/rebase-apply")
$detached = (git symbolic-ref -q HEAD) -eq $null
if ($rebasing) {
git rebase --abort 2>$null
"sync-failed: rebase aborted at $(Get-Date -Format o)" | Out-File -Append .sync-status
} elseif ($detached) {
git checkout main --quiet 2>$null
"sync-failed: recovered from detached HEAD at $(Get-Date -Format o)" | Out-File -Append .sync-status
}
exit 0
Because the previous session already committed its changes locally before the pull ran, aborting a failed rebase only discards the failed replay — never the memory itself.
cd $env:MEMORY_REPO
git add -A
git commit -m "session $(Get-Date -Format o)" --quiet 2>$null
git push --quiet
if ($LASTEXITCODE -eq 0) {
Remove-Item .sync-status -ErrorAction SilentlyContinue
} else {
"push-failed at $(Get-Date -Format o)" | Out-File -Append .sync-status
}
Why this matters: A successful pull doesn't prove sync is healthy — a successful round trip does. The marker file only clears on a real push, so a broken sync keeps surfacing at the next session start instead of disappearing after one quiet failure.
Why it matters here
None of this is Optimizely-specific — it's git and two PowerShell scripts. It matters for a project like this one because Optimizely platforms live for years and accumulate real tribal knowledge: which reindex is safe to run unattended, which scheduled jobs have bitten you before, which feature flags are dead weight nobody's removed, which migration has an ordering dependency nobody wrote down. An agent that keeps that knowledge across machines is worth a lot more than one that resets every time you switch machines.
What to set up
- Memory folder → its own git repo, remote pushed once
- Plans folder → its own git repo, outside any application repo
- SessionStart hook → pull, then verify repo state and mark failures persistently
- SessionEnd hook → commit, push, clear the marker only on a successful push
Curious whether others syncing AI memory across machines have run into the same failure mode, or found a cleaner way to detect it. I'd like to compare notes.
Comments