Contents
claude-sync is a bash script that backs up and synchronises Claude Code data (memory files, settings, CLAUDE.md files, scripts) across devices via a private GitHub repository. It uses git as the transport layer — each push is a signed commit with a timestamp and device name, giving you a full history of every change.
# Core flow
~/.claude/ ──push──▶ github.com/d1g1entr0py/claude-data ──pull──▶ ~/.claude/ (other device)
hetzner-cloudvm-hel1). Manual commands are only needed when you want an immediate sync or are setting up a new machine.
Source path (~/.claude/…) | Repo path | What it is |
|---|---|---|
| projects/-/memory/ | memory/global/ | All global memory files — user profile, feedback rules, project notes, references |
| projects/<name>/memory/ | projects/<name>/memory/ | Per-project memory (auto-discovered for every project that has a memory/ dir) |
| projects/<name>/CLAUDE.md | projects/<name>/CLAUDE.md | Per-project instruction files (auto-discovered) |
| settings.json | settings.json | Global Claude Code settings (theme, model, permissions, hooks, etc.) |
| scripts/ | scripts/ | Custom scripts including claude-sync.sh itself |
| todos/ | todos/ | Task tracking files |
| skills/*/SKILL.md skills/*/*.json skills/*/*.md | skills/ | Skill manifests and reference docs (not node_modules or caches) |
| Path | Reason |
|---|---|
| plugins/ | npm-installed, machine-specific — reinstall on each device |
| projects/*/<session>.jsonl | Session conversation files — large, synced separately on demand via sessions push/pull |
| cache/, paste-cache/, telemetry/ | Ephemeral / machine-specific |
| debug/ | Debug logs — not portable |
| .sync-token | Secret — never committed |
.jsonl) are excluded from the default push because they can be hundreds of MB. Use claude-sync sessions push when you specifically want to move conversation history between devices.
All commands are run as: bash ~/.claude/scripts/claude-sync.sh <command> — or just claude-sync <command> if symlinked to PATH (see below).
| Command | Cadence | What it does |
|---|---|---|
| push [message] | auto 30m | Stage all changed files, commit, push to GitHub. Optional message appended to commit. |
| pull | manual | Pull latest from GitHub and restore files to ~/.claude/. Use on a new/secondary device after another machine has pushed. |
| status | manual | Show pending local changes, how many commits ahead/behind remote, and last 5 sync history entries. |
| log [N] | manual | Show last N sync commits (default 20). Each entry includes device name and timestamp. |
| setup | once | First-time init: verifies token, clones repo, configures git identity. Run once per device. |
| auto [minutes] | once | Installs a cron job to auto-push every N minutes (default 30). Already active on this server. |
| auto-off | manual | Removes the auto-push cron job. |
| sessions push | manual | On-demand push of all .jsonl session files. Large — run deliberately, not on a schedule. |
| sessions pull | manual | Restore session files from the repo. Only copies files that don't already exist locally. |
# Check what would be pushed before committing
bash ~/.claude/scripts/claude-sync.sh status
# Immediate push with a label (e.g. end of a session)
bash ~/.claude/scripts/claude-sync.sh push "completed voice-assistant refactor"
# Pull latest on a secondary device after the server has auto-pushed
bash ~/.claude/scripts/claude-sync.sh pull
# View sync history
bash ~/.claude/scripts/claude-sync.sh log 10
# Add to PATH for convenience (run once)
ln -s ~/.claude/scripts/claude-sync.sh /usr/local/bin/claude-sync
claude-sync status
The auto-push cron is already installed on hetzner-cloudvm-hel1:
*/30 * * * * /root/.claude/scripts/claude-sync.sh push >> /root/.claude/.sync.log 2>&1
Output is logged to ~/.claude/.sync.log. To tail it:
tail -f ~/.claude/.sync.log
To change the interval (e.g. every 15 minutes):
bash ~/.claude/scripts/claude-sync.sh auto-off
bash ~/.claude/scripts/claude-sync.sh auto 15
Run these commands on the new machine:
# 1. Create the token file (use the same token as the server, or create a new one)
echo 'ghp_YOURTOKEN' > ~/.claude/.sync-token && chmod 600 ~/.claude/.sync-token
# 2. Copy the script to the right location
mkdir -p ~/.claude/scripts
# Option A — copy from this server over SSH:
scp -i ~/.ssh/vps-hetzner root@89.167.54.62:~/.claude/scripts/claude-sync.sh ~/.claude/scripts/
# Option B — download from GitHub (requires token):
TOKEN=$(cat ~/.claude/.sync-token) && curl -s -H "Authorization: Bearer $TOKEN" \
"https://raw.githubusercontent.com/d1g1entr0py/claude-data/main/scripts/claude-sync.sh" \
-o ~/.claude/scripts/claude-sync.sh
chmod +x ~/.claude/scripts/claude-sync.sh
# 3. Run setup — clones the repo and configures git
bash ~/.claude/scripts/claude-sync.sh setup
# 4. Pull current state
bash ~/.claude/scripts/claude-sync.sh pull
# 5. Optionally enable auto-push on this device too
bash ~/.claude/scripts/claude-sync.sh auto 30
# 6. Optionally add to PATH
ln -s ~/.claude/scripts/claude-sync.sh /usr/local/bin/claude-sync
pull, Claude Code will load the synced memory and settings on next launch. No restart of any service is needed — Claude reads memory files fresh at the start of each session.
Open the script and find the SYNC_MAP array near the top:
nano ~/.claude/scripts/claude-sync.sh
# or
vim ~/.claude/scripts/claude-sync.sh
Add an entry to SYNC_MAP. Format is ["source relative to ~/.claude"]="dest in repo":
declare -A SYNC_MAP=(
# existing entries...
["settings.json"]="settings.json"
["projects/-/memory"]="memory/global"
["scripts"]="scripts"
["todos"]="todos"
# NEW — example: sync a custom hooks directory
["hooks"]="hooks"
)
Directories are synced with rsync -a --delete (mirrors the source exactly). Single files are copied directly.
Delete the line from SYNC_MAP. Note: files already committed to the repo are not automatically deleted from GitHub — you'd need to cd ~/.claude/.sync-repo && git rm -r <path> && git push if you want to clean them out.
You don't need to add projects manually. The discover_projects() function scans ~/.claude/projects/ and automatically includes any directory that has a memory/ subdirectory or a CLAUDE.md file. Creating either of those is enough for the project to be picked up on the next push.
The script uses rsync for directory syncing. To exclude files, edit the stage_files() function and add --exclude='pattern' to the relevant rsync call:
rsync -a --delete --exclude='.git' --exclude='*.tmp' --exclude='secrets.json' \
"$src_path/" "$dst_path/"
The token is stored in ~/.claude/.sync-token (mode 600, never committed to git). Every network operation reads this file and updates the git remote URL automatically — so rotating the token is a one-liner:
# Rotate the token — script self-heals on next run
echo 'ghp_NEWTOKEN' > ~/.claude/.sync-token && chmod 600 ~/.claude/.sync-token
# Verify it works
bash ~/.claude/scripts/claude-sync.sh status
claude-sync-hetzner), choose an expiryrepo scope (full repository access)echo 'ghp_...' > ~/.claude/.sync-token && chmod 600 ~/.claude/.sync-token.sync-token file. All tokens must have repo scope on d1g1entr0py/claude-data.
| Symptom | Cause | Fix |
|---|---|---|
| Permission denied in sync log | Script lost execute bit (happens when git commits it as 100644) | chmod +x ~/.claude/scripts/claude-sync.sh |
| Authentication failed | Token expired or rotated | Update ~/.claude/.sync-token with fresh token — script auto-refreshes remote URL |
| Another sync is running (PID…) | Stale lock file from a crashed run | rm ~/.claude/.sync.lock |
| Push rejected (fetch first) | Remote has commits the local repo doesn't — typically after a push from another device | Run claude-sync pull then claude-sync push — or let the next cron tick handle it (pull is always first in push) |
| dubious ownership error | Repo owned by a different user than the git process | git config --global --add safe.directory ~/.claude/.sync-repo |
| Sync log is empty / no activity | Cron not installed, or nothing has changed since last push | Check crontab -l | grep claude-sync. If missing: claude-sync auto 30 |
| Memory not appearing on second device after pull | Claude Code caches memory at session start | Start a new Claude Code session — memory is read fresh each time, no restart needed |
cd ~/.claude/.sync-repo
# Show last 10 commits
git log --oneline -10
# See exactly what changed in the last push
git show --stat HEAD
# Check remote is reachable
GIT_TERMINAL_PROMPT=0 git ls-remote origin HEAD
# See what would be staged right now
bash ~/.claude/scripts/claude-sync.sh status
A full PowerShell port of the bash script is available. It mirrors all commands — push, pull, status, log, auto — and uses Task Scheduler instead of cron and robocopy instead of rsync.
git to PATH)repo scope — see section 7.Windows blocks unsigned scripts by default. Set the policy once per machine (run PowerShell as Administrator):
# Allow local scripts — run once as Administrator
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine
# 1. Create the Claude scripts directory
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.claude\scripts"
# 2. Download the script
Invoke-WebRequest -Uri "https://env.consaul.cloud/claude-sync.ps1" `
-OutFile "$env:USERPROFILE\.claude\scripts\claude-sync.ps1"
# 3. Save your GitHub token
Set-Content "$env:USERPROFILE\.claude\.sync-token" "ghp_YOURTOKEN"
# 4. Run setup — verifies git, token, and clones the repo
& "$env:USERPROFILE\.claude\scripts\claude-sync.ps1" setup
# 5. Pull current state from GitHub
& "$env:USERPROFILE\.claude\scripts\claude-sync.ps1" pull
# 6. Enable auto-push every 30 minutes via Task Scheduler
& "$env:USERPROFILE\.claude\scripts\claude-sync.ps1" auto 30
# Shorthand alias — run once per session or add to $PROFILE
Set-Alias claude-sync "$env:USERPROFILE\.claude\scripts\claude-sync.ps1"
# Then use the same command names as the bash version:
claude-sync status
claude-sync push "end of session note"
claude-sync pull
claude-sync log 10
claude-sync auto 30 # register Task Scheduler job
claude-sync auto-off # remove Task Scheduler job
Set-Alias line to your PowerShell profile:notepad $PROFILE — create the file if it doesn't exist.
| Concept | Linux / macOS | Windows |
|---|---|---|
| Claude dir | ~/.claude/ | %USERPROFILE%\.claude\ |
| Token file | ~/.claude/.sync-token | %USERPROFILE%\.claude\.sync-token |
| Sync repo | ~/.claude/.sync-repo/ | %USERPROFILE%\.claude\.sync-repo\ |
| Log file | ~/.claude/.sync.log | %USERPROFILE%\.claude\.sync.log |
| Script location | ~/.claude/scripts/claude-sync.sh | %USERPROFILE%\.claude\scripts\claude-sync.ps1 |
| Auto-sync | crontab -l | Task Scheduler → task named ClaudeSync |
| Directory mirror | rsync -a --delete | robocopy /E /PURGE (built-in) |
| File permissions | chmod 600 .sync-token | ACL locked to current user by setup |
# Check if the job is registered and its last run time
Get-ScheduledTask -TaskName ClaudeSync | Select-Object TaskName, State
(Get-ScheduledTaskInfo -TaskName ClaudeSync).LastRunTime
(Get-ScheduledTaskInfo -TaskName ClaudeSync).LastTaskResult # 0 = success
# Trigger a manual run immediately (same as push)
Start-ScheduledTask -TaskName ClaudeSync
# Remove the job
& "$env:USERPROFILE\.claude\scripts\claude-sync.ps1" auto-off
# Tail the log
Get-Content "$env:USERPROFILE\.claude\.sync.log" -Wait -Tail 20
# Open your profile file (creates it if missing)
if (-not (Test-Path $PROFILE)) { New-Item $PROFILE -Force }
notepad $PROFILE
# Add this line, save, then re-open PowerShell:
Set-Alias claude-sync "$env:USERPROFILE\.claude\scripts\claude-sync.ps1"
| Symptom | Cause | Fix |
|---|---|---|
| cannot be loaded because running scripts is disabled | Execution policy blocks unsigned scripts | Set-ExecutionPolicy RemoteSigned -Scope LocalMachine (as Admin) |
| git : The term 'git' is not recognized | Git for Windows not in PATH | Install Git for Windows; restart PowerShell |
| robocopy exits non-zero but files copied OK | robocopy uses exit codes 0–7 as success bitmasks (not errors) | Expected behaviour — the script treats exit codes ≤7 as success |
| Task Scheduler job shows LastTaskResult 0x1 | PowerShell can't find the script path at task runtime | Re-run auto command — it re-registers with the current full path |
| Authentication failed on push | Rotated token; script reads from file each run | Set-Content "$env:USERPROFILE\.claude\.sync-token" "ghp_NEWTOKEN" |
Last updated 2026-04-23 · env.consaul.cloud · Bash: ~/.claude/scripts/claude-sync.sh · PowerShell: claude-sync.ps1