
Mem Sync
- 25 installs
- Updated August 4, 2026
- akunzai/agent-skills
mem-sync is a Claude Code skill that synchronizes a project's .memories/ daily logs across devices over Git using an isolated per-user branch and worktree.
About
mem-sync synchronizes a project's .memories/ daily logs across machines over Git using a dedicated per-user branch and a background worktree, so the active development branch is never disturbed. It pulls other devices' logs at session start and pushes local logs at session end. A developer uses it to keep agent memory consistent across devices. The sync branch is derived from git user.email so developers on the same repo never collide.
- Synchronizes project .memories/ across machines over Git using an isolated per-user branch and worktree
- Never pollutes the active development branch with memory logs
- Derives a per-user branch memories/<email-localpart> so two developers never collide on same-date log filenames
Mem Sync by the numbers
- 25 all-time installs (skills.sh)
- Ranked #370 of 733 Git & Pull Requests skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
mem-sync capabilities & compatibility
- Capabilities
- memory recall · memory promote · memory setup
- Works with
- github · gitlab
- Use cases
- memory · devops
- Platforms
- macOS · Linux · Windows
- Pricing
- Free
What mem-sync says it does
The sync branch is derived from `git config user.email`: the part before `@`, lowercased and slugified
Never commit `.memories/` to an ordinary development branch.
npx skills add https://github.com/akunzai/agent-skills --skill mem-syncAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 25 |
|---|---|
| Last updated | August 4, 2026 |
| Repository | akunzai/agent-skills ↗ |
What it does
Sync a project's .memories/ daily logs across devices over Git on an isolated per-user branch.
Who is it for?
Syncing agent memory logs across machines without touching the dev branch
Skip if: Committing .memories/ to an ordinary development branch
When should I use this skill?
Pulling other devices' logs at session start or pushing local logs at session end
What you get
Memory logs sync across devices on an isolated per-user branch, leaving the dev branch untouched.
- Synced .memories/ snapshot across devices
- A dedicated memories/<user> branch
By the numbers
- 6 commands (pull, push, status, diff, print-branch, print-remote)
Files
mem-sync — Cross-Device Memory Sync
Synchronizes <repo>/.memories/ across machines through a dedicated, isolated per-user branch memories/<email-localpart> using a background git worktree, so the active development branch and your editor workspace are never disturbed.
Per-User Branch
The sync branch is derived from git config user.email: the part before @, lowercased and slugified (non-alphanumeric runs become -). User@Example.com → memories/user. Each user pushes/pulls only their own branch, so two developers on the same repo never collide on same-date log filenames.
Windows
On Windows, run this skill's script through Git Bash (Git for Windows). A PATH guard at the top of the script prepends the MSYS usr/bin so find/tar are not shadowed by the native same-name tools; if a required POSIX tool is still missing, the script aborts with an explicit error naming the missing tool.
Commands
Discover the script path dynamically (do NOT hardcode): 1. Global scope: ~/.agents/skills/mem-sync/scripts/mem-sync-git.sh 2. Project scope: <repo>/skills/mem-sync/scripts/mem-sync-git.sh 3. Fallback: search for mem-sync-git.sh.
mem-sync-git.sh pull— record local WIP, fetch+rebase remote, copy back. Run at session start.mem-sync-git.sh push— same merge, then push. Run at session end or after promoting candidates.mem-sync-git.sh(no argument) — defaults tostatus.mem-sync-git.sh status— read-only: print the resolved remote/branch header, then summarize how local.memories/differs (in sync, or local-only / remote-only / modified files).mem-sync-git.sh diff— read-only: likestatusbut prints the full unified diff (local vs remote).mem-sync-git.sh print-branch— print the derived per-user branch and exit (machine-readable, used by tooling/tests).mem-sync-git.sh print-remote— resolve and print just the sync remote (env → auto-detect) and exit (machine-readable).statusalready shows this; useprint-remotewhen a script needs the bare value. Exits non-zero with guidance if the remote is ambiguous.
See references/git-sync-workflow.md for mechanics, anti-pollution rules, and conflict handling.
Read-After-Sync Ordering
Run pull, push, and compact as exclusive operations against .memories/. Do not read .memories/ in parallel with those commands.
pull briefly removes and recreates the local .memories/ directory while copying the synchronized snapshot back from the isolated worktree. A parallel grep, Select-String, Get-Content, cat, or similar read can observe that transient missing-directory state and report a false error. Wait for the sync command to finish successfully before scanning handoffs, candidates, or daily logs.
Remote resolution
The sync remote is resolved in this order:
1. `MEM_SYNC_REMOTE` env var — explicit one-off override; highest priority, never persisted. 2. Auto-detect from the repo's own push configuration (not by remote name):
- the current branch's push target resolved by Git itself
(git for-each-ref --format='%(push:remotename)': branch.<name>.pushRemote → remote.pushDefault → tracking remote) → use that remote, so memory follows wherever the repo actually pushes (e.g. a fork's writable remote), regardless of the name origin;
- no push target configured but exactly one remote exists → use it;
- no push target and multiple remotes → ambiguous: the command lists the remotes and
exits non-zero. Give the current branch a push target (git push -u <remote> <branch>) or rerun with MEM_SYNC_REMOTE=<name>, then retry.
The remote is recomputed from live Git config on every run, so retargeting your push remote is picked up automatically — there is no stored memory-sync remote to go stale. The per-user branch name (memories/<email-localpart>) is unaffected by the remote choice.
If a command reports an ambiguous remote set, relay the printed remote list to the user and ask which remote to use, then set a push target (or MEM_SYNC_REMOTE=<name>) before retrying.
Anti-Pollution
Never commit .memories/ to an ordinary development branch. If the user asks to "commit memory" or "sync daily notes to Git", intercept and route through this syncer.
Git Memories Synchronization Workflow
This reference explains how to use the automated sync script to seamlessly synchronize daily logs (.memories/) across multiple machines without polluting the main branch or messing up your editor's workspace.
1. Dynamic Discovery Algorithm (For AI Agents)
Since this skill can be loaded in different scopes, DO NOT hardcode the script path. Determine the absolute path dynamically:
1. User Scope Check: If loaded globally, the script is at ~/.agents/skills/mem-sync/scripts/mem-sync-git.sh. 2. Project Scope Check: If loaded locally, the script is at <repo>/skills/mem-sync/scripts/mem-sync-git.sh. 3. Fallback Discovery: If both checks fail, use workspace file search tools to search for mem-sync-git.sh in the workspace or ~/.agents/ directory.
2. Core Mechanics
Instead of switching your active working branch (which triggers editor resets and file reloading), the automated script uses Git Worktrees in the background:
- It creates a dedicated, isolated per-user branch named
memories/<email-localpart>with no parent history (an orphan branch). - It checks out this branch into a hidden workspace folder
.git/memories-worktree/. - Anti-Loss Sync Rule (3-Way Rebase): When syncing, the script first records the local
.memories/snapshot as a WIP commit inside the isolated worktree, then fetches and rebases onto<remote>/memories/<email-localpart>(the resolved sync remote). This lets Git merge concurrent daily-log edits without relying on filesystem modification times. - Deletion Semantics (push vs pull):
pushmirrors the local.memories/exactly, so a file removed locally is recorded as a deletion and propagates to other devices.pullis non-destructive: it overlays local WIP (new/modified files) onto the remote snapshot but never records a deletion for a file that is merely absent locally. This keeps a pull on a fresh device — or one whose.memories/was emptied by a local clean — from wiping the remote logs of other devices. - Conflict Safety: If Git reports a rebase conflict, the script stops without copying conflicted files back to the local workspace. Resolve the conflict inside
.git/memories-worktree/, rungit rebase --continueor abort, then rerun sync.
3. Command Line Operations
You can run the script manually depending on the scope:
Upload & Push Local Notes
# Example: Executing globally
~/.agents/skills/mem-sync/scripts/mem-sync-git.sh pushpush also fetches and rebases remote updates before pushing. Even when there are no new local changes, it can still bring another device's latest daily logs back into the local .memories/ directory.
Download & Pull Remote Notes
# Example: Executing globally
~/.agents/skills/mem-sync/scripts/mem-sync-git.sh pullpull records unpushed local daily notes before rebasing remote changes, so it is not a blind overwrite operation. Unlike push, it never propagates a local deletion: files absent locally are restored from the remote rather than removed from the branch.
Inspect Differences (read-only)
# Per-file summary: in sync, or local-only / remote-only / modified
~/.agents/skills/mem-sync/scripts/mem-sync-git.sh status
# Full unified diff of local .memories/ vs the remote per-user branch
~/.agents/skills/mem-sync/scripts/mem-sync-git.sh diffstatus/diff only fetch the per-user branch and compare it against the local .memories/ directory — they never modify the working copy, the worktree, or the remote. If the remote branch does not exist yet, status reports how many local logs are unpushed.
3a. Remote Resolution
The remote is resolved as: MEM_SYNC_REMOTE env var → auto-detect. Auto-detect follows the repo's own push configuration instead of guessing by remote name:
- it resolves the current branch's push target via Git itself
(git for-each-ref --format='%(push:remotename)', i.e. branch.<name>.pushRemote → remote.pushDefault → tracking remote) and uses that remote — so memory follows wherever this repo actually pushes (a fork's writable remote, etc.), independent of the name origin;
- if no push target is configured but exactly one remote exists → use it;
- otherwise (no push target and multiple remotes) → the command lists the remotes and
aborts so nothing is pushed to the wrong place.
# Normal case: the remote is taken from where the current branch pushes.
git push -u myfork "$(git branch --show-current)" # if not set up yet
~/.agents/skills/mem-sync/scripts/mem-sync-git.sh push
# One-off override for an ambiguous repo or a different target:
MEM_SYNC_REMOTE=memvault ~/.agents/skills/mem-sync/scripts/mem-sync-git.sh pushThe remote is recomputed from live Git config each run, so retargeting your push remote is picked up automatically — nothing is stored for memory sync to go stale. The env-sourced remote is taken verbatim; read-only status/diff never write config. The resolved remote must exist or the script aborts, naming the resolution source. The per-user branch name (memories/<email-localpart>) is unaffected.
4. Automation Guidelines for AI Agents
Whenever you start a session or detect active git operations in a cross-device project environment:
- Proactively offer to execute
mem-sync-git.sh pull(using the discovered path) to pull down notes recorded on other devices. - Before ending a session or after promoting candidate memories, execute
mem-sync-git.sh pushto persist today's notes for other workspaces.
Authoritative Compaction (compact)
mem-sync-git.sh compact rewrites the user's memories/<email-localpart> branch to a single orphan commit containing only the current local .memories/ files, then force-pushes it. Use it for short-term cleanup (see the mem-clean skill): delete expired logs locally, run pull first to gather all of this user's devices' logs, then compact.
Because it rewrites history, other devices detect the missing common ancestor on their next pull and adopt the rewrite via reset --hard instead of rebasing — so deletions propagate instead of resurrecting. This is per-user and never affects other developers.
Anti-Pollution Rules
- Never commit
.memories/to an ordinary development branch (main,dev,feature/*). - If the user asks to "commit memory" or "sync daily notes to Git", intercept and
explain that daily logs sync through the isolated per-user memories/<email-localpart> branch via this syncer, not the active branch.
- Always use
mem-sync-git.sh(running inside the isolated memory worktree) to sync.
#!/usr/bin/env bash
# Unified Memory Autopilot - Cross-device Git Worktree Syncer
#
# This script synchronizes the local '.memories/' directory with a dedicated,
# isolated per-user git branch ('memories/<email-localpart>') using git worktree.
# It ensures sync without switching your active development branch.
set -euo pipefail
# >>> posix-path-guard >>>
# On Windows (Git Bash / MSYS / Cygwin), make MSYS coreutils win over the
# native find.exe/tar.exe in C:\Windows\System32. Keep this block identical
# across scripts (verified by tests/windows-path-guard.sh).
case "${OSTYPE:-}" in
msys*|cygwin*)
if [ -x /usr/bin/sed ]; then
PATH="/usr/bin:/bin:$PATH"
elif command -v git >/dev/null 2>&1; then
_git_root="$(dirname "$(dirname "$(command -v git)")")"
[ -x "$_git_root/usr/bin/sed" ] && PATH="$_git_root/usr/bin:$_git_root/bin:$PATH"
unset _git_root
fi
;;
esac
# <<< posix-path-guard <<<
_msg_missing=""
for _tool in sed grep tr mktemp date cp wc diff find tar; do
command -v "$_tool" >/dev/null 2>&1 || _msg_missing="$_msg_missing $_tool"
done
if [ -n "$_msg_missing" ]; then
echo "Error: required POSIX tool(s) not found:$_msg_missing" >&2
echo "On Windows, run mem-sync through Git Bash (Git for Windows) so its usr/bin tools are on PATH." >&2
exit 1
fi
unset _msg_missing _tool
MEMORY_PATH=".memories"
MEMORY_ATTRS_RULE="$MEMORY_PATH/** text eol=lf"
# Check if git repository exists
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Error: Not a git repository."
exit 1
fi
REPO_DIR="$(git rev-parse --show-toplevel)"
GIT_COMMON_DIR="$(git -C "$REPO_DIR" rev-parse --git-common-dir)"
case "$GIT_COMMON_DIR" in
/*) ;;
*) GIT_COMMON_DIR="$REPO_DIR/$GIT_COMMON_DIR" ;;
esac
derive_branch() {
local email slug
email="$(git -C "$REPO_DIR" config user.email 2>/dev/null || true)"
slug="$(printf '%s' "${email%%@*}" \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')"
if [ -z "$slug" ]; then
echo "Error: git config user.email is required to derive the per-user memory branch." >&2
exit 1
fi
printf 'memories/%s' "$slug"
}
BRANCH="$(derive_branch)"
REMOTE=""
REMOTE_SOURCE=""
WORKTREE_DIR="$GIT_COMMON_DIR/memories-worktree"
LOCAL_DIR="$REPO_DIR/$MEMORY_PATH"
resolve_remote() {
# 1. explicit one-off override (never persisted)
if [ -n "${MEM_SYNC_REMOTE:-}" ]; then
REMOTE="$MEM_SYNC_REMOTE"
REMOTE_SOURCE="env"
return
fi
# 2. auto-detect from the repo's own push configuration
local remotes count
remotes="$(git -C "$REPO_DIR" remote)"
count="$(printf '%s\n' "$remotes" | grep -c . || true)"
if [ "$count" -eq 0 ]; then
echo "Error: no git remote is configured for memory sync." >&2
echo "Add one (e.g. 'git remote add origin <url>') or set MEM_SYNC_REMOTE=<name>." >&2
exit 1
fi
# 2a. Honor Git's own push-remote resolution for the current branch
# (branch.<name>.pushRemote -> remote.pushDefault -> tracking remote).
# Name-independent: it follows wherever this repo actually pushes.
# for-each-ref's %(push:remotename) derives the destination purely from
# config, so it works even when the remote-tracking ref is absent locally
# and yields the remote name directly (no parsing). Detached HEAD -> empty.
local br cand
br="$(git -C "$REPO_DIR" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
if [ -n "$br" ]; then
cand="$(git -C "$REPO_DIR" for-each-ref --format='%(push:remotename)' "refs/heads/$br" 2>/dev/null || true)"
if [ -n "$cand" ] && printf '%s\n' "$remotes" | grep -qx "$cand"; then
REMOTE="$cand"
REMOTE_SOURCE="auto-push"
return
fi
fi
# 2b. Single remote: unambiguous fallback when no push remote is configured yet.
if [ "$count" -eq 1 ]; then
REMOTE="$remotes"
REMOTE_SOURCE="auto-single"
return
fi
# 2c. Multiple remotes and no push target for the current branch: refuse to guess.
echo "Error: cannot determine the memory sync remote automatically." >&2
echo "The current branch has no push remote and several remotes exist:" >&2
printf '%s\n' "$remotes" | sed 's/^/ - /' >&2
echo "Set a push remote (e.g. 'git push -u <remote> <branch>')," >&2
echo "or run a one-off: MEM_SYNC_REMOTE=<name> <command>" >&2
exit 1
}
ensure_remote() {
if ! git -C "$REPO_DIR" remote get-url "$REMOTE" >/dev/null 2>&1; then
echo "Error: remote '$REMOTE' (from $REMOTE_SOURCE) does not exist but is required for memory sync." >&2
echo "Fix it with 'git remote add $REMOTE <url>' or run 'MEM_SYNC_REMOTE=<name> <command>'." >&2
exit 1
fi
}
create_memory_branch() {
local init_worktree
init_worktree="$(mktemp -d "$GIT_COMMON_DIR/memories-init.XXXXXX")"
rmdir "$init_worktree"
git -C "$REPO_DIR" worktree add --detach "$init_worktree" HEAD >/dev/null
(
cd "$init_worktree"
git checkout --orphan "$BRANCH" >/dev/null
git rm -rf . >/dev/null 2>&1 || true
mkdir -p "$MEMORY_PATH"
touch "$MEMORY_PATH/.gitkeep"
printf '%s\n' "$MEMORY_ATTRS_RULE" > .gitattributes
git add -f "$MEMORY_PATH/.gitkeep" .gitattributes
git commit -m "Initialize memory sync branch" >/dev/null
git push "$REMOTE" "$BRANCH" >/dev/null
)
git -C "$REPO_DIR" worktree remove --force "$init_worktree" >/dev/null
}
setup_worktree() {
ensure_remote
if [ ! -d "$WORKTREE_DIR" ]; then
echo "Initializing isolated memory worktree..."
# Fetch the remote branch if it exists but is not present locally
if git ls-remote --exit-code --heads "$REMOTE" "$BRANCH" >/dev/null 2>&1; then
echo "Fetching remote branch '$BRANCH'..."
git fetch "$REMOTE" "$BRANCH":"$BRANCH" || git fetch "$REMOTE" "$BRANCH"
fi
# Create orphan branch if it doesn't exist remotely or locally
if ! git rev-parse --verify "$BRANCH" >/dev/null 2>&1; then
echo "Creating fresh orphan branch '$BRANCH'..."
create_memory_branch
fi
# Add worktree linked to the isolated branch
git worktree add -f "$WORKTREE_DIR" "$BRANCH"
fi
}
ensure_clean_worktree() {
local git_dir
git_dir="$(git -C "$WORKTREE_DIR" rev-parse --git-dir)"
case "$git_dir" in
/*) ;;
*) git_dir="$WORKTREE_DIR/$git_dir" ;;
esac
if [ -d "$git_dir/rebase-merge" ] || [ -d "$git_dir/rebase-apply" ] || [ -f "$git_dir/MERGE_HEAD" ]; then
echo "Error: Memory sync worktree has an unfinished merge or rebase at $WORKTREE_DIR." >&2
echo "Resolve it there and continue, or run 'git -C \"$WORKTREE_DIR\" rebase --abort' before syncing again." >&2
exit 1
fi
if [ -n "$(git -C "$WORKTREE_DIR" status --porcelain)" ]; then
echo "Error: Memory sync worktree has uncommitted changes at $WORKTREE_DIR." >&2
echo "Resolve or discard them before syncing again." >&2
exit 1
fi
}
commit_local_snapshot() {
# propagate_deletions=true (push): mirror local exactly, so files removed
# locally are recorded as deletions and propagate upstream.
# propagate_deletions=false (pull): overlay local onto the remote snapshot
# already checked out in the worktree, so local WIP (new/modified files) is
# preserved while files merely ABSENT locally are NOT deleted. Pull must never
# turn an empty or partial local '.memories/' (fresh device, post-clean) into
# an authoritative deletion of remote files.
local propagate_deletions="${1:-true}"
if [ ! -d "$LOCAL_DIR" ]; then
echo "No local '$MEMORY_PATH' directory found. Only remote memories will be synced."
return
fi
if [ "$propagate_deletions" = "true" ]; then
rm -rf "${WORKTREE_DIR:?}/$MEMORY_PATH"
fi
mkdir -p "$WORKTREE_DIR/$MEMORY_PATH"
cp -R "$LOCAL_DIR/." "$WORKTREE_DIR/$MEMORY_PATH/"
# Per-device sentinel: must not propagate to other machines. On push, strip it
# so it leaves the branch. On pull, leave the branch's copy untouched so the
# snapshot stages no sentinel-only deletion (pull must stay a no-op when only
# the sentinel differs); the copy-back step keeps it from reaching local.
if [ "$propagate_deletions" = "true" ]; then
rm -f "$WORKTREE_DIR/$MEMORY_PATH/.handoff-migrated"
fi
# Ensure empty subdirectories are tracked (e.g. handoffs/ with no active tasks)
find "$WORKTREE_DIR/$MEMORY_PATH" -mindepth 1 -type d -empty -exec touch '{}/.gitkeep' \;
touch "$WORKTREE_DIR/$MEMORY_PATH/.gitkeep"
printf '%s\n' "$MEMORY_ATTRS_RULE" > "$WORKTREE_DIR/.gitattributes"
git -C "$WORKTREE_DIR" -c core.safecrlf=false add -f "$MEMORY_PATH/" .gitattributes
if git -C "$WORKTREE_DIR" diff --cached --quiet; then
echo "No local daily log changes detected."
else
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
git -C "$WORKTREE_DIR" commit -m "sync: local daily memories at $TIMESTAMP"
fi
}
rebase_remote() {
echo "Rebasing memory worktree with remote '$BRANCH'..."
git -C "$WORKTREE_DIR" fetch "$REMOTE" "$BRANCH"
if ! git -C "$WORKTREE_DIR" merge-base HEAD "$REMOTE/$BRANCH" >/dev/null 2>&1; then
echo "Remote '$BRANCH' was rewritten (no common ancestor); adopting it authoritatively."
git -C "$WORKTREE_DIR" reset --hard "$REMOTE/$BRANCH"
return
fi
if ! git -C "$WORKTREE_DIR" rebase "$REMOTE/$BRANCH"; then
echo "Conflict detected in $WORKTREE_DIR." >&2
echo "Resolve conflicts there, run 'git rebase --continue', then rerun this sync command." >&2
echo "Local '$MEMORY_PATH' was not overwritten." >&2
exit 1
fi
}
sync_back_to_local() {
if [ -d "$WORKTREE_DIR/$MEMORY_PATH" ]; then
# The .handoff-migrated sentinel is per-device: never import another
# machine's copy carried on the branch, and preserve only this device's own.
local had_migrated=false
if [ -f "$LOCAL_DIR/.handoff-migrated" ]; then had_migrated=true; fi
rm -rf "${LOCAL_DIR:?}"
mkdir -p "$LOCAL_DIR"
cp -R "$WORKTREE_DIR/$MEMORY_PATH/." "$LOCAL_DIR/"
find "$LOCAL_DIR" -name '.gitkeep' -delete 2>/dev/null || true
rm -f "$LOCAL_DIR/.handoff-migrated"
if $had_migrated; then touch "$LOCAL_DIR/.handoff-migrated"; fi
fi
}
sync_merge_remote() {
# First arg selects deletion semantics for commit_local_snapshot:
# push propagates local deletions; pull does not. Defaults to push.
local propagate_deletions="${1:-true}"
setup_worktree
ensure_clean_worktree
commit_local_snapshot "$propagate_deletions"
rebase_remote
}
sync_push() {
echo "Syncing local memories -> remote '$BRANCH'..."
sync_merge_remote true
git -C "$WORKTREE_DIR" push "$REMOTE" "$BRANCH"
sync_back_to_local
echo "Successfully pushed daily memories to remote."
}
sync_pull() {
echo "Syncing remote '$BRANCH' -> local memories..."
sync_merge_remote false
if [ -d "$WORKTREE_DIR/$MEMORY_PATH" ]; then
sync_back_to_local
echo "Successfully pulled and updated local daily memories."
else
echo "No remote daily memories found."
fi
}
sync_compact() {
echo "Compacting '$BRANCH' to a single authoritative commit..."
setup_worktree
ensure_clean_worktree
# Replace worktree contents with the current local snapshot.
rm -rf "${WORKTREE_DIR:?}/$MEMORY_PATH"
mkdir -p "$WORKTREE_DIR/$MEMORY_PATH"
if [ -d "$LOCAL_DIR" ]; then
cp -R "$LOCAL_DIR/." "$WORKTREE_DIR/$MEMORY_PATH/"
fi
touch "$WORKTREE_DIR/$MEMORY_PATH/.gitkeep"
printf '%s\n' "$MEMORY_ATTRS_RULE" > "$WORKTREE_DIR/.gitattributes"
# Rebuild history as a single orphan commit.
# Clean up any stale compact-tmp branch left by a prior interrupted run.
git -C "$WORKTREE_DIR" checkout "$BRANCH" >/dev/null 2>&1 || true
git -C "$WORKTREE_DIR" branch -D "compact-tmp" >/dev/null 2>&1 || true
git -C "$WORKTREE_DIR" checkout --orphan "compact-tmp" >/dev/null
git -C "$WORKTREE_DIR" -c core.safecrlf=false add -Af
git -C "$WORKTREE_DIR" commit -m "compact: authoritative memory snapshot $(date '+%Y-%m-%d %H:%M:%S')" >/dev/null
git -C "$WORKTREE_DIR" branch -M "$BRANCH"
git -C "$WORKTREE_DIR" push --force "$REMOTE" "$BRANCH"
echo "Force-pushed compacted '$BRANCH'. Other devices will adopt it on next sync."
}
sync_status() {
# Read-only: compares local .memories/ against the remote per-user branch.
# mode "summary" prints a per-file overview; mode "diff" prints the full unified diff.
local mode="${1:-summary}"
ensure_remote
# Surface the resolved remote/branch up front (folds in 'print-remote'), so a
# plain status answers "which remote would a sync target?" without a 2nd call.
if [ "$mode" = "summary" ]; then
echo "Remote: $REMOTE ($REMOTE_SOURCE) Branch: $BRANCH"
fi
if ! git -C "$REPO_DIR" ls-remote --exit-code --heads "$REMOTE" "$BRANCH" >/dev/null 2>&1; then
echo "Remote branch '$BRANCH' does not exist on '$REMOTE' yet."
if [ -d "$LOCAL_DIR" ]; then
local n
n="$(find "$LOCAL_DIR" -type f ! -name '.gitkeep' 2>/dev/null | wc -l | tr -d ' ')"
echo "Local has $n daily log file(s) not yet pushed."
fi
return 0
fi
git -C "$REPO_DIR" fetch -q "$REMOTE" "$BRANCH"
local tmp
tmp="$(mktemp -d)"
trap 'rm -rf "${tmp:-}"' RETURN
mkdir -p "$tmp/$MEMORY_PATH" "$LOCAL_DIR"
git -C "$REPO_DIR" archive "$REMOTE/$BRANCH" -- "$MEMORY_PATH" 2>/dev/null | tar -x -C "$tmp" 2>/dev/null || true
# Exclude the .gitkeep placeholder: it stays on the branch but pull strips it
# locally, so it would otherwise always show as a spurious remote-only diff.
# Ignore CRLF/LF-only differences: legacy memory branches may contain CRLF
# blobs from Windows clones before the .gitattributes rule was restored.
if [ "$mode" = "diff" ]; then
diff -ru --strip-trailing-cr -x '.gitkeep' -x '.handoff-migrated' "$tmp/$MEMORY_PATH" "$LOCAL_DIR" || true
return 0
fi
local out
out="$(diff -rq --strip-trailing-cr -x '.gitkeep' -x '.handoff-migrated' "$tmp/$MEMORY_PATH" "$LOCAL_DIR" 2>/dev/null || true)"
if [ -z "$out" ]; then
echo "In sync with '$REMOTE/$BRANCH'."
else
echo "Differences vs '$REMOTE/$BRANCH' (run 'diff' for full content):"
printf '%s\n' "$out" \
| sed -e "s#Only in $tmp/$MEMORY_PATH:# remote-only:#" \
-e "s#Only in $LOCAL_DIR:# local-only :#" \
-e "s#^Files .*/\(.*\) and .* differ# modified : \1#"
fi
}
case "${1:-status}" in
push)
resolve_remote
sync_push
;;
pull)
resolve_remote
sync_pull
;;
compact)
resolve_remote
sync_compact
;;
status)
resolve_remote
sync_status summary
;;
diff)
resolve_remote
sync_status diff
;;
print-branch)
printf '%s\n' "$BRANCH"
;;
print-remote)
resolve_remote
printf '%s\n' "$REMOTE"
;;
*)
echo "Usage: $0 {push|pull|compact|status|diff|print-branch|print-remote}"
exit 1
;;
esac
Related skills
FAQ
How does mem-sync avoid collisions between developers?
It derives a per-user branch memories/<email-localpart> from git user.email, so each user pushes and pulls only their own branch.
Which commands does it provide?
pull, push, status, diff, print-branch, and print-remote, run as exclusive operations against .memories/.