- bin/ssr-update: fast-forward `git pull` on the clone with safety guards (dirty tree, detached HEAD, divergent history all abort). - Re-renders & reloads the launchd plist when launchd/ or bin/ssr* changed AND the schedule is currently active, so the cron picks up any plist template updates automatically. - Reports VERSION delta, commit log, and new config keys introduced in config/ssr.conf.example so users can opt-in via `ssr config edit`. - `--dry-run` shows would-be changes without pulling or touching launchd. - Wired into bin/ssr dispatcher + README updated (Updating section, layout entry, command help line). Co-authored-by: Cursor <cursoragent@cursor.com>
8.5 KiB
ssr — System Sync & Restore for macOS
Reproducible macOS setup in one command. ssr snapshots your Homebrew bundle and application configs to a cloud drive on a schedule, and can rebuild a fresh Mac from that snapshot.
- Backup (
ssr sync): dumpsBrewfileand runsmackup backupto the configured cloud directory. - Restore (
ssr restore): installs Homebrew, replays theBrewfile, restoresmackupconfigs, applies macOS defaults. - Scheduled sync: native macOS
launchdagent (daily by default). - Cloud storage: iCloud Drive, Dropbox, Google Drive, OneDrive, or any custom path — selectable via config.
- No secrets: all state lives in your cloud drive; the repo only contains code and config templates.
Successor to the original
system_sync.sh/system_restore.sh. Same intent, modernized tooling, modular, idempotent, scheduled by default.
Requirements
- macOS (Apple Silicon or Intel)
- Bash 3.2+ (preinstalled)
- Network access for the initial Homebrew install
- A cloud drive folder you control (iCloud Drive is the default)
Install
git clone https://git.surke.de/osurke/system-sync-restore.git ~/.ssr
cd ~/.ssr
./install.sh
install.sh will:
- Symlink
bin/ssrinto~/.local/bin(override withSSR_PREFIX). - Seed
~/.config/ssr/ssr.conffromconfig/ssr.conf.example(chmod 600). - Optionally enable the scheduled daily sync.
If ~/.local/bin is not on your PATH, add it to your shell rc:
export PATH="$HOME/.local/bin:$PATH"
Usage
ssr install First-time install: link binary, seed config, schedule sync
ssr sync One-shot backup → cloud
ssr restore [Brewfile] Restore the machine; Brewfile defaults to the cloud copy
ssr update [--dry-run] Pull latest ssr code, refresh launchd if needed
ssr schedule on|off Enable/disable the daily launchd job
ssr config show|edit Show or edit the active config
ssr status Show config, cloud target, last sync, schedule state
ssr version Print version
First backup
ssr sync
Output structure in the cloud drive:
<cloud root>/
├── BACKUP/
│ └── <hostname> - <hardware-uuid>/
│ ├── Brewfile
│ └── Brewfile.bak
└── Mackup/
└── … (managed by mackup)
Restore on a new machine
git clone https://git.surke.de/osurke/system-sync-restore.git ~/.ssr
cd ~/.ssr && ./install.sh
ssr restore
By default ssr restore picks the Brewfile from <cloud>/BACKUP/<host> - <uuid>/Brewfile. To restore from a different machine's snapshot, pass an explicit path:
ssr restore "$HOME/Library/Mobile Documents/com~apple~CloudDocs/BACKUP/oldhost - ABCD-1234/Brewfile"
Configuration
Config lives in ~/.config/ssr/ssr.conf (override with --config or $SSR_CONFIG). The file is sourced by bash but only recognised KEY=VALUE lines are accepted; anything else causes ssr to abort.
| Key | Default | Purpose |
|---|---|---|
SSR_CLOUD_PROVIDER |
icloud |
icloud|dropbox|googledrive|onedrive|custom |
SSR_CLOUD_PATH |
— | Absolute path; only used when provider is custom |
SSR_BACKUP_SUBDIR |
BACKUP |
Subfolder for per-host backups |
SSR_MACKUP_SUBDIR |
Mackup |
Subfolder used by mackup |
SSR_BREW_UPGRADE |
true |
Run brew upgrade during sync |
SSR_BREW_CLEANUP |
true |
Run brew cleanup after sync |
SSR_DISABLE_GATEKEEPER |
false |
Run spctl --global-disable during restore (security trade-off) |
SSR_OPEN_CASKS |
false |
Open every installed cask after restore for first-run config |
SSR_MACOS_DEFAULTS |
repo's config/macos-defaults.sh |
Override path to your own defaults script |
SSR_LAUNCHD_LABEL |
de.surke.ssr.sync |
launchd job label |
SSR_SCHEDULE_HOUR |
9 |
Daily run hour (0–23, local time) |
SSR_SCHEDULE_MINUTE |
0 |
Daily run minute |
SSR_PREFIX |
$HOME/.local/bin |
Install prefix for the ssr symlink |
SSR_STATE_DIR |
$HOME/.local/state/ssr |
Internal state directory |
SSR_LOG_DIR |
$HOME/.local/state/ssr/logs |
launchd log directory |
Cloud provider paths resolved automatically
| Provider | Resolved root |
|---|---|
icloud |
~/Library/Mobile Documents/com~apple~CloudDocs |
dropbox |
~/Library/CloudStorage/Dropbox* (fallback ~/Dropbox) |
googledrive |
~/Library/CloudStorage/GoogleDrive-* |
onedrive |
~/Library/CloudStorage/OneDrive-* |
custom |
Value of SSR_CLOUD_PATH |
Scheduled sync
ssr schedule on # write & load ~/Library/LaunchAgents/<label>.plist
ssr schedule off # unload & remove
ssr schedule status # check
Logs land in $SSR_LOG_DIR/sync.out.log and sync.err.log. Tail them:
tail -f ~/.local/state/ssr/logs/sync.err.log
Show launchd-level details:
launchctl list | grep ssr
log show --predicate 'process == "ssr"' --last 1h
Updating
Updates happen on three independent layers:
Tool code (this repo). Self-update via ssr update:
ssr update # git pull --ff-only on the clone, then refresh side-effects
ssr update --dry-run # show what would change, don't pull
ssr update will:
- Refuse if the working tree is dirty or HEAD is detached.
- Fast-forward only — never rewrites history.
- Re-render and reload the launchd plist if
launchd/orbin/ssr*changed and the schedule is currently active. - Print added config keys when
config/ssr.conf.examplegained new options.
Backup data (cloud drive). Refreshed automatically by the scheduled ssr sync. The previous Brewfile is preserved as Brewfile.bak; deeper history is provided by the cloud provider's built-in version retention (iCloud Drive, Dropbox, …). Trigger an on-demand sync with ssr sync.
Dependencies (Homebrew, mackup). ssr sync runs brew upgrade and brew cleanup when SSR_BREW_UPGRADE=true / SSR_BREW_CLEANUP=true (both default true). Set them to false in ssr.conf to keep snapshots without auto-upgrading installed packages.
Project layout
.
├── install.sh # Thin wrapper around bin/ssr-install
├── bin/
│ ├── ssr # CLI dispatcher
│ ├── ssr-install # Local install + scheduling
│ ├── ssr-update # Self-update (git pull + launchd refresh)
│ ├── ssr-sync # Backup to cloud
│ ├── ssr-restore # Rebuild a Mac from the backup
│ ├── ssr-schedule # launchd on/off/status
│ ├── ssr-config # show/edit/path
│ └── ssr-status # state summary
├── lib/
│ ├── common.sh # Logging, validation, config loader
│ ├── cloud.sh # Provider abstraction
│ ├── brew.sh # Homebrew install + Brewfile ops
│ ├── mackup.sh # Mackup install + backup/restore
│ └── macos.sh # macOS-specific actions (Rosetta, defaults, etc.)
├── config/
│ ├── ssr.conf.example # Reference user config
│ └── macos-defaults.sh # Default macOS preferences
├── launchd/
│ └── de.surke.ssr.sync.plist # Template for the scheduled agent
├── VERSION
├── LICENSE
└── README.md
All dependencies are inside the repository; the only external requirements are macOS itself and curl (for the one-time Homebrew bootstrap).
Security
- No secrets in this repo. All cloud data lives in the user's cloud drive.
- The config file is sourced but validated: only
# commentsandKEY=VALUElines are allowed. Foreign content aborts execution. chmod 600is applied tossr.confand the rendered launchd plist.SSR_DISABLE_GATEKEEPER=trueis opt-in only; off by default.- No remote pipes (
curl … | bash) except Homebrew's own bootstrap during install.
Uninstall
ssr schedule off
rm -f ~/.local/bin/ssr
rm -rf ~/.config/ssr ~/.local/state/ssr
The cloud data is left untouched.
Migration from the legacy scripts
The old system_sync.sh / system_restore.sh map onto the new commands:
| Legacy | New |
|---|---|
system_sync.sh |
ssr sync |
system_restore.sh <Brewfile> |
ssr restore <Brewfile> |
| Manual cron / Lingon entry | ssr schedule on |
| Hard-coded iCloud path | SSR_CLOUD_PROVIDER in ssr.conf |
Inline .macos block |
config/macos-defaults.sh (overridable) |
License
MIT. See LICENSE.