claude-sync — How To Guide

Script: ~/.claude/scripts/claude-sync.sh  ·  Repo: github.com/d1g1entr0py/claude-data  ·  ← env.consaul.cloud

Contents

  1. Overview
  2. What Is Synced
  3. Commands
  4. Automation (cron)
  5. Setting Up a New Device
  6. Adding / Removing Sync Locations
  7. Managing the GitHub Token
  8. Troubleshooting
  9. Windows Deployment (PowerShell)

1 — Overview

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)
The script runs every 30 minutes automatically via cron on this server (hetzner-cloudvm-hel1). Manual commands are only needed when you want an immediate sync or are setting up a new machine.

2 — What Is Synced

Included

Source path (~/.claude/…)Repo pathWhat 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.mdprojects/<name>/CLAUDE.mdPer-project instruction files (auto-discovered)
settings.jsonsettings.jsonGlobal 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)

Not Included (excluded by design)

PathReason
plugins/npm-installed, machine-specific — reinstall on each device
projects/*/<session>.jsonlSession conversation files — large, synced separately on demand via sessions push/pull
cache/, paste-cache/, telemetry/Ephemeral / machine-specific
debug/Debug logs — not portable
.sync-tokenSecret — never committed
Session files (.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.

3 — Commands

All commands are run as: bash ~/.claude/scripts/claude-sync.sh <command> — or just claude-sync <command> if symlinked to PATH (see below).

CommandCadenceWhat it does
push [message]auto 30mStage all changed files, commit, push to GitHub. Optional message appended to commit.
pullmanualPull latest from GitHub and restore files to ~/.claude/. Use on a new/secondary device after another machine has pushed.
statusmanualShow pending local changes, how many commits ahead/behind remote, and last 5 sync history entries.
log [N]manualShow last N sync commits (default 20). Each entry includes device name and timestamp.
setuponceFirst-time init: verifies token, clones repo, configures git identity. Run once per device.
auto [minutes]onceInstalls a cron job to auto-push every N minutes (default 30). Already active on this server.
auto-offmanualRemoves the auto-push cron job.
sessions pushmanualOn-demand push of all .jsonl session files. Large — run deliberately, not on a schedule.
sessions pullmanualRestore session files from the repo. Only copies files that don't already exist locally.

Common usage examples

# 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

4 — Automation (cron)

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
If the cron stops working silently, the most likely cause is a rotated GitHub token. See the Token section — the script now auto-refreshes the git remote URL from the token file, so updating the file is all that's needed.

5 — Setting Up a New Device

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
After 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.

6 — Adding / Removing Sync Locations

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

Adding a static path

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.

Removing a static path

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.

Project memory and CLAUDE.md files (auto-discovered)

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.

Excluding specific files from a synced directory

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/"

7 — Managing the GitHub Token

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

Creating a token on GitHub

  1. Go to github.com → Settings → Developer settings → Personal access tokens → Tokens (classic)
  2. Click Generate new token (classic)
  3. Set a note (e.g. claude-sync-hetzner), choose an expiry
  4. Check the repo scope (full repository access)
  5. Click Generate token, copy it immediately
  6. Save to the server: echo 'ghp_...' > ~/.claude/.sync-token && chmod 600 ~/.claude/.sync-token
If you use different tokens per device, each device needs its own .sync-token file. All tokens must have repo scope on d1g1entr0py/claude-data.

8 — Troubleshooting

SymptomCauseFix
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

Manually inspect the sync repo

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

9 — Windows Deployment (PowerShell)

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.

Download the script directly: env.consaul.cloud/claude-sync.ps1

Prerequisites

Execution policy (one-time)

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

First-time setup

# 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

Commands (PowerShell syntax)

# 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
To make the alias permanent, add the Set-Alias line to your PowerShell profile:
notepad $PROFILE — create the file if it doesn't exist.

Path differences: Linux vs Windows

ConceptLinux / macOSWindows
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-synccrontab -lTask Scheduler → task named ClaudeSync
Directory mirrorrsync -a --deleterobocopy /E /PURGE (built-in)
File permissionschmod 600 .sync-tokenACL locked to current user by setup

Viewing and managing the Task Scheduler job

# 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

Adding the alias to your PowerShell profile (permanent)

# 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"

Troubleshooting (Windows-specific)

SymptomCauseFix
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