Created
June 8, 2026 18:04
-
-
Save feliperohdee/35be04923990a30e405de2c4a9643985 to your computer and use it in GitHub Desktop.
claude bkp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # | |
| # Backup of Claude Code + Claude Desktop (cowork) durable state. | |
| # Allowlist approach: copies ONLY what is hard to recreate (instructions, | |
| # settings, skills, plugin config, and all `memory/` dirs). Everything | |
| # ephemeral or secret (caches, telemetry, sessions, cookies, tokens) is | |
| # left out on purpose — it regenerates on the next login. | |
| # | |
| set -euo pipefail | |
| REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| CLAUDE_HOME="$HOME/.claude" | |
| DESKTOP_HOME="$HOME/Library/Application Support/Claude" | |
| DOCS_HOME="$HOME/Documents/Claude" | |
| DOT_DEST="$REPO/dot-claude" | |
| COWORK_DEST="$REPO/cowork" | |
| DOCS_DEST="$REPO/documents-claude" | |
| RSYNC_EXCLUDES=(--exclude='.DS_Store' --exclude='*.tmp' --exclude='*.lock') | |
| # Copy every `memory/` directory found under $1 into $2, preserving the | |
| # relative path so the structure can be restored verbatim. | |
| copy_memories() { | |
| local src="$1" dest="$2" | |
| [ -d "$src" ] || return 0 | |
| ( cd "$src" && find . -type d -name memory -print0 ) | while IFS= read -r -d '' dir; do | |
| mkdir -p "$dest/$dir" | |
| rsync -a "${RSYNC_EXCLUDES[@]}" "$src/$dir/" "$dest/$dir/" | |
| done | |
| } | |
| # Fresh snapshot every run so deletions propagate to the backup. | |
| rm -rf "$DOT_DEST" "$COWORK_DEST" "$DOCS_DEST" | |
| mkdir -p "$DOT_DEST" | |
| echo "==> ~/.claude essentials" | |
| # Top-level config files: global instructions, settings and keybindings. | |
| for f in CLAUDE.md settings.json settings.local.json keybindings.json CLAUDE.local.md; do | |
| if [ -f "$CLAUDE_HOME/$f" ]; then | |
| cp "$CLAUDE_HOME/$f" "$DOT_DEST/$f" | |
| fi | |
| done | |
| echo "==> ~/.claude config dirs (skills + agents/commands/output-styles/rules if present)" | |
| # Whole config directories: your custom skills, subagents, slash commands, | |
| # output styles and path-scoped rules (only the ones that exist are copied). | |
| for d in skills agents commands output-styles rules; do | |
| if [ -d "$CLAUDE_HOME/$d" ]; then | |
| rsync -a "${RSYNC_EXCLUDES[@]}" "$CLAUDE_HOME/$d/" "$DOT_DEST/$d/" | |
| fi | |
| done | |
| echo "==> ~/.claude/plugins config (lets you reinstall the same plugins)" | |
| # Plugin *manifest* only: which marketplaces are known + which plugins are | |
| # installed. The plugin code lives in git-cloned marketplaces and is fetched | |
| # again from these files on restore — like package.json, not node_modules. | |
| mkdir -p "$DOT_DEST/plugins" | |
| for f in known_marketplaces.json installed_plugins.json; do | |
| if [ -f "$CLAUDE_HOME/plugins/$f" ]; then | |
| cp "$CLAUDE_HOME/plugins/$f" "$DOT_DEST/plugins/$f" | |
| fi | |
| done | |
| echo "==> user-scoped MCP servers (extracted from ~/.claude.json, no tokens/caches)" | |
| # ~/.claude.json mixes durable config with OAuth tokens, caches and per-project | |
| # history — so we pull ONLY the `mcpServers` key into a small standalone file. | |
| if [ -f "$HOME/.claude.json" ]; then | |
| python3 - "$HOME/.claude.json" "$DOT_DEST/mcp-servers.json" <<'PY' | |
| import json, sys | |
| src, dest = sys.argv[1], sys.argv[2] | |
| data = json.load(open(src)) | |
| mcp = data.get("mcpServers") or {} | |
| if mcp: | |
| json.dump({"mcpServers": mcp}, open(dest, "w"), indent=2, sort_keys=True) | |
| print(f" saved {len(mcp)} MCP server(s)") | |
| else: | |
| print(" no user-scoped MCP servers") | |
| PY | |
| fi | |
| echo "==> ~/.claude/projects memories" | |
| copy_memories "$CLAUDE_HOME/projects" "$DOT_DEST/projects" | |
| mkdir -p "$COWORK_DEST" | |
| echo "==> Claude Desktop config (globalShortcut, preferences, MCP servers)" | |
| # Desktop app config: keyboard shortcut, UI preferences (trusted folders, | |
| # starred spaces...) and Desktop MCP servers. No auth tokens live here. | |
| if [ -f "$DESKTOP_HOME/claude_desktop_config.json" ]; then | |
| cp "$DESKTOP_HOME/claude_desktop_config.json" "$COWORK_DEST/claude_desktop_config.json" | |
| fi | |
| echo "==> cowork (Claude Desktop) memories" | |
| copy_memories "$DESKTOP_HOME/local-agent-mode-sessions" "$COWORK_DEST/local-agent-mode-sessions" | |
| echo "==> cowork registries (spaces.json, settings, scheduled tasks)" | |
| # spaces.json maps a space id -> name/folder. Without it a restored | |
| # `spaces/<id>/memory` is orphaned: cowork wouldn't know that space exists. | |
| # The account/org path levels above it are your account & org UUIDs, which are | |
| # recreated identically on the same login — so the space memories reconnect. | |
| if [ -d "$DESKTOP_HOME/local-agent-mode-sessions" ]; then | |
| ( cd "$DESKTOP_HOME/local-agent-mode-sessions" \ | |
| && find . -maxdepth 3 \( -name spaces.json -o -name cowork_settings.json -o -name scheduled-tasks.json \) -print0 ) \ | |
| | while IFS= read -r -d '' f; do | |
| mkdir -p "$COWORK_DEST/local-agent-mode-sessions/$(dirname "$f")" | |
| cp "$DESKTOP_HOME/local-agent-mode-sessions/$f" "$COWORK_DEST/local-agent-mode-sessions/$f" | |
| done | |
| fi | |
| echo "==> ~/Documents/Claude (cowork space project & scheduled folders)" | |
| # This is where cowork spaces keep their working files (spaces.json folder | |
| # paths point here). Backing it up makes restored spaces self-contained. | |
| if [ -d "$DOCS_HOME" ]; then | |
| rsync -a "${RSYNC_EXCLUDES[@]}" "$DOCS_HOME/" "$DOCS_DEST/" | |
| fi | |
| echo "==> done." | |
| # Append-only run log: one line per execution (date + time). Lives at the repo | |
| # root so the snapshot wipe above never touches it, and it gets committed below. | |
| printf '%s\n' "$(date '+%Y-%m-%d %H:%M:%S')" >> "$REPO/log.txt" | |
| # Commit & push the snapshot (always commits — the log line just changed). | |
| cd "$REPO" | |
| git add -A | |
| git diff --cached --quiet || { git commit -m "backup $(date +%Y-%m-%d_%H:%M)" && git push; } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # | |
| # Restore Claude Code + Claude Desktop (cowork) durable state onto this machine. | |
| # Merges the backup into your live dirs WITHOUT deleting anything already there, | |
| # so it is safe to run on a fresh install or an existing setup. | |
| # | |
| # Run AFTER you have logged into Claude Code / Desktop at least once (so the | |
| # base dirs and fresh auth tokens exist). This only puts back the durable bits. | |
| # | |
| set -euo pipefail | |
| REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| CLAUDE_HOME="$HOME/.claude" | |
| DESKTOP_HOME="$HOME/Library/Application Support/Claude" | |
| DOCS_HOME="$HOME/Documents/Claude" | |
| DOT_SRC="$REPO/dot-claude" | |
| COWORK_SRC="$REPO/cowork" | |
| DOCS_SRC="$REPO/documents-claude" | |
| echo "This will merge the backup into:" | |
| echo " $CLAUDE_HOME" | |
| echo " $DESKTOP_HOME" | |
| echo " $DOCS_HOME" | |
| read -r -p "Continue? [y/N] " ans | |
| case "$ans" in | |
| y|Y) ;; | |
| *) echo "Aborted."; exit 1 ;; | |
| esac | |
| mkdir -p "$CLAUDE_HOME" | |
| echo "==> ~/.claude (CLAUDE.md, settings, skills, plugins config, memories)" | |
| # mcp-servers.json is merged into ~/.claude.json below, not dropped here as-is. | |
| rsync -a --exclude='mcp-servers.json' "$DOT_SRC/" "$CLAUDE_HOME/" | |
| if [ -f "$DOT_SRC/mcp-servers.json" ]; then | |
| echo "==> merging user-scoped MCP servers into ~/.claude.json" | |
| python3 - "$DOT_SRC/mcp-servers.json" "$HOME/.claude.json" <<'PY' | |
| import json, os, sys | |
| backup, target = sys.argv[1], sys.argv[2] | |
| servers = json.load(open(backup)).get("mcpServers", {}) | |
| data = json.load(open(target)) if os.path.exists(target) else {} | |
| data.setdefault("mcpServers", {}).update(servers) | |
| json.dump(data, open(target, "w"), indent=2) | |
| print(f" merged {len(servers)} MCP server(s)") | |
| PY | |
| fi | |
| if [ -f "$COWORK_SRC/claude_desktop_config.json" ]; then | |
| echo "==> merging Claude Desktop config" | |
| mkdir -p "$DESKTOP_HOME" | |
| python3 - "$COWORK_SRC/claude_desktop_config.json" "$DESKTOP_HOME/claude_desktop_config.json" <<'PY' | |
| import json, os, sys | |
| backup, target = sys.argv[1], sys.argv[2] | |
| src = json.load(open(backup)) | |
| data = json.load(open(target)) if os.path.exists(target) else {} | |
| data.update(src) | |
| json.dump(data, open(target, "w"), indent=2) | |
| print(" done") | |
| PY | |
| fi | |
| if [ -d "$COWORK_SRC/local-agent-mode-sessions" ]; then | |
| echo "==> cowork memories + registries (spaces.json, settings, scheduled tasks)" | |
| mkdir -p "$DESKTOP_HOME/local-agent-mode-sessions" | |
| rsync -a "$COWORK_SRC/local-agent-mode-sessions/" "$DESKTOP_HOME/local-agent-mode-sessions/" | |
| fi | |
| if [ -d "$DOCS_SRC" ]; then | |
| echo "==> ~/Documents/Claude (cowork space project & scheduled folders)" | |
| mkdir -p "$DOCS_HOME" | |
| rsync -a "$DOCS_SRC/" "$DOCS_HOME/" | |
| fi | |
| echo "==> done. Restart Claude Code / Desktop to pick up the restored state." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # | |
| # Enable / disable the daily backup schedule (runs backup.sh at 20:00). | |
| # Uses macOS launchd, so a missed run (laptop asleep) fires on next wake. | |
| # | |
| # Usage: | |
| # ./schedule.sh enable # install + activate the 20:00 daily schedule | |
| # ./schedule.sh disable # deactivate + remove the schedule | |
| # ./schedule.sh run # run backup.sh now (to test) | |
| # ./schedule.sh status # show whether the schedule is active | |
| # | |
| set -euo pipefail | |
| REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| LABEL="com.feliperohde.claude-backup" | |
| PLIST="$HOME/Library/LaunchAgents/$LABEL.plist" | |
| DOMAIN="gui/$(id -u)" | |
| HOUR=20 | |
| MINUTE=0 | |
| usage() { | |
| cat <<EOF | |
| schedule.sh — manage the daily Claude backup schedule | |
| WHAT IT DOES | |
| Schedules backup.sh to run every day at $(printf '%02d:%02d' "$HOUR" "$MINUTE") | |
| via macOS launchd. Because it uses launchd (not cron), a run missed while the | |
| laptop is asleep fires automatically on the next wake. | |
| COMMANDS | |
| enable Generate the launchd plist and activate the daily schedule. | |
| disable Deactivate the schedule and remove the plist. | |
| run Trigger backup.sh right now (use this to test). | |
| status Show whether the schedule is currently active. | |
| FILES | |
| Schedule : $PLIST | |
| Logs : /tmp/claude-backup.log (output) | |
| /tmp/claude-backup.err (errors — check this first if something fails) | |
| BEFORE ENABLING — two prerequisites, or it runs but nothing reaches GitHub: | |
| 1. Commit/push: uncomment the 'git add/commit/push' block at the end of | |
| backup.sh, and make sure 'git push' works without a password (SSH key or | |
| a stored credential), since it runs unattended. | |
| 2. Full Disk Access: grant it to /bin/bash in | |
| System Settings > Privacy & Security > Full Disk Access — otherwise macOS | |
| blocks a background job from reading ~/Documents and Application Support. | |
| EXAMPLE | |
| ./schedule.sh enable # turn it on | |
| ./schedule.sh run # test immediately | |
| cat /tmp/claude-backup.err | |
| EOF | |
| } | |
| write_plist() { | |
| mkdir -p "$HOME/Library/LaunchAgents" | |
| cat > "$PLIST" <<EOF | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>Label</key> | |
| <string>$LABEL</string> | |
| <key>ProgramArguments</key> | |
| <array> | |
| <string>/bin/bash</string> | |
| <string>$REPO/backup.sh</string> | |
| </array> | |
| <key>StartCalendarInterval</key> | |
| <dict> | |
| <key>Hour</key> | |
| <integer>$HOUR</integer> | |
| <key>Minute</key> | |
| <integer>$MINUTE</integer> | |
| </dict> | |
| <key>StandardOutPath</key> | |
| <string>/tmp/claude-backup.log</string> | |
| <key>StandardErrorPath</key> | |
| <string>/tmp/claude-backup.err</string> | |
| </dict> | |
| </plist> | |
| EOF | |
| } | |
| case "${1:-}" in | |
| enable) | |
| write_plist | |
| launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true | |
| launchctl bootstrap "$DOMAIN" "$PLIST" | |
| launchctl enable "$DOMAIN/$LABEL" | |
| printf '✓ enabled — backup.sh runs daily at %02d:%02d\n' "$HOUR" "$MINUTE" | |
| echo " test it now with: ./schedule.sh run" | |
| ;; | |
| disable) | |
| launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true | |
| rm -f "$PLIST" | |
| echo "✓ disabled — schedule removed" | |
| ;; | |
| run) | |
| launchctl kickstart -k "$DOMAIN/$LABEL" \ | |
| && echo "✓ started — check /tmp/claude-backup.log and /tmp/claude-backup.err" \ | |
| || { echo "not enabled yet — run ./schedule.sh enable first"; exit 1; } | |
| ;; | |
| status) | |
| if launchctl list | grep -q "$LABEL"; then | |
| echo "active:" | |
| launchctl list | grep "$LABEL" | |
| else | |
| echo "inactive (not scheduled)" | |
| fi | |
| ;; | |
| "") | |
| usage | |
| ;; | |
| *) | |
| echo "unknown command: $1" | |
| echo | |
| usage | |
| exit 1 | |
| ;; | |
| esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment