#!/bin/sh # Move Claude Code's project memory between this repo and the local machine. # # tools/memory-sync.sh export memory on this machine -> .claude/memory/ # tools/memory-sync.sh import .claude/memory/ -> this machine # tools/memory-sync.sh status show both sides, change nothing # # Why this exists # --------------- # Claude Code keeps per-project memory outside the repo, under # # /.claude/projects//memory/ # # where is the project's absolute path with the separators replaced by # dashes: D:\DEV\SchOS becomes D--DEV-SchOS, /home/me/dev/SchOS becomes # -home-me-dev-SchOS. Two consequences follow, and both have already bitten # this project once: # # * Renaming the directory orphans the memory. D:\DEV\Schunix -> D:\DEV\SchOS # changed the key, and everything written under the old one became # invisible until it was moved across by hand. # * The key is machine-specific, so memory written on Windows is not where # Linux looks for it, even for the same checkout. # # Committing the memory to the repo fixes both: git carries it between hosts # and across renames, and `import` puts it wherever this particular machine # expects to find it. Run `export` after a session that learned something and # `import` after cloning or pulling on another machine. set -eu usage() { echo "usage: $0 {export|import|status}" >&2 exit 2 } [ $# -eq 1 ] || usage # Repo root: this script lives in tools/, so the parent of its directory. script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) repo=$(CDPATH= cd -- "$script_dir/.." && pwd) repo_mem="$repo/.claude/memory" # The absolute path Claude Code itself sees. Under Git Bash `pwd` gives # /d/DEV/SchOS, but Claude Code is a Windows program and uses D:\DEV\SchOS, so # the key must be derived from the Windows form or it will not match. native_path="$repo" case "$(uname -s)" in MINGW* | MSYS* | CYGWIN*) if command -v cygpath >/dev/null 2>&1; then native_path=$(cygpath -w "$repo") fi ;; esac # Encode: every separator becomes a dash, and so does the drive colon. # # D:\DEV\SchOS -> D--DEV-SchOS # /home/me/dev/SchOS -> -home-me-dev-SchOS # # The colon maps to a dash rather than being deleted -- that is where the # doubled dash in the Windows form comes from, and dropping it instead yields # D-DEV-SchOS, which matches nothing. key=$(printf '%s' "$native_path" | tr '/\\:' '---') home_dir=${HOME:-$USERPROFILE} host_mem="$home_dir/.claude/projects/$key/memory" count() { [ -d "$1" ] && find "$1" -name '*.md' 2>/dev/null | wc -l | tr -d ' ' || echo 0; } case "$1" in status) echo "repo: $repo" echo "as Claude sees it: $native_path" echo "project key: $key" echo echo "in repo: $repo_mem" echo " $(count "$repo_mem") memories" echo "on this host: $host_mem" echo " $(count "$host_mem") memories" ;; export) if [ ! -d "$host_mem" ]; then echo "memory-sync: nothing to export -- no memory at $host_mem" >&2 echo " (is the project key right? run: $0 status)" >&2 exit 1 fi mkdir -p "$repo_mem" # Copy rather than mirror: deleting from the repo is a git operation and # should be visible in a diff, not a side effect of running a sync script. cp "$host_mem"/*.md "$repo_mem"/ 2>/dev/null || true echo "exported $(count "$repo_mem") memories to .claude/memory/" echo "commit them to carry them to your other machines." ;; import) if [ ! -d "$repo_mem" ]; then echo "memory-sync: nothing to import -- no .claude/memory/ in this repo" >&2 exit 1 fi mkdir -p "$host_mem" cp "$repo_mem"/*.md "$host_mem"/ 2>/dev/null || true echo "imported $(count "$host_mem") memories to:" echo " $host_mem" ;; *) usage ;; esac