SchOS/tools/memory-sync.sh
Nexoniarz 3182da3965 Carry project memory in the repo, with tools/memory-sync.sh
Claude Code keeps per-project memory at <home>/.claude/projects/<key>/,
where <key> is the project's absolute path with every separator and the
drive colon turned into a dash. That makes it both rename-fragile and
machine-specific, and this project has hit each of those once:

  - D:\DEV\Schunix -> D:\DEV\SchOS changed the key, and 43 memories were
    invisible until moved across by hand.
  - Development happens from Windows and from Linux, and each looks in a
    different place for the same checkout.

Committing .claude/memory/ fixes both -- git carries it across renames
and between hosts -- and `import` puts it wherever the local machine
expects. .claude/settings.local.json stays ignored: those are per-machine
permissions.

The colon maps to a dash rather than being dropped. Deleting it gives
D-DEV-SchOS, which matches nothing and silently finds zero memories; that
was the first attempt. On Git Bash the key comes from `cygpath -w`,
because Claude Code is a Windows program and sees D:\DEV\SchOS, not
/d/DEV/SchOS.
2026-08-09 14:27:22 +02:00

111 lines
3.8 KiB
Shell

#!/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
#
# <home>/.claude/projects/<key>/memory/
#
# where <key> 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