Instead of silently writing ~/.mackup/ssr-*.cfg on every sync, generation is now an explicit opt-in command: ssr mackup scan ssr mackup scan — dry-run: shows NEW/UPD/DEL changes, prompts before writing ssr mackup list — list existing generated files ssr mackup clean — remove all generated files (with confirmation) ssr sync no longer calls generate_custom_cfg automatically. Completions updated for zsh, bash, fish. Co-authored-by: Cursor <cursoragent@cursor.com>
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 completions install Install zsh/bash/fish tab-completion
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
│ ├── unmanaged-apps.md # apps installed manually (not brew/MAS)
│ └── macos-prefs/ # pref-domain snapshots
│ ├── com.apple.dock.plist
│ ├── com.apple.finder.plist
│ └── …
└── Mackup/
└── … (managed by mackup)
During each sync the unmanaged-apps scan compares every .app bundle in /Applications (and ~/Applications) against:
- Mac App Store — bundle contains
Contents/_MASReceipt/receipt - Homebrew Cask — bundle filename matches the
.appartifact of any installed cask
Anything not matched is written to unmanaged-apps.md (Markdown table, one row per app, with version, bundle id and a best-effort installer link) and printed to the sync log as a warning. Disable with SSR_SCAN_UNMANAGED_APPS=false.
Link selection for each app uses, in order:
- Sparkle feed —
SUFeedURLfrom the bundle'sInfo.plist→ linked vendor homepage. - Known vendor map — bundle-id prefix maps to the official download page (Adobe, Microsoft, Docker, Cursor, Anthropic, JetBrains, GitKraken, Signal, Zoom, etc.).
- Web search — DuckDuckGo query for
<App name> macOS download.
macOS preferences snapshot
ssr sync also snapshots the live state of a curated set of macOS preference domains, writing one plist per domain to <backup_dir>/macos-prefs/. During ssr restore these are replayed after config/macos-defaults.sh, so the snapshot is the last writer.
Default domains:
| Domain | What it covers |
|---|---|
com.apple.dock |
Dock position, app layout, hot corners |
com.apple.finder |
View options, sidebar, path bar |
com.apple.symbolichotkeys |
Global keyboard shortcuts (Spotlight, Mission Control, Screenshots…) |
com.apple.HIToolbox |
Input sources / keyboard layouts |
com.apple.spaces |
Spaces / Mission Control layout |
com.apple.menuextra.clock |
Menu-bar clock format |
NSGlobalDomain |
Global UI prefs, key-repeat speed, sound, language |
Conflict resolution during restore
When a snapshot domain overlaps with a domain that config/macos-defaults.sh also writes to, ssr restore warns and asks interactively:
[WARN] Pref-snapshot overlaps with declarative defaults for 2 domain(s):
com.apple.dock, com.apple.finder
Domain `com.apple.dock`:
Snapshot from last sync (2026-05-11T17:30:00+0200) will OVERRIDE values
set by macos-defaults.sh.
[A]ccept snapshot / [r]eject (keep declarative)? [A/r]:
Default = Accept (press Enter). Reply r to keep the declarative value for that domain. In non-interactive contexts (launchd, CI) the decision falls back to SSR_PREFS_CONFLICT_DEFAULT (default accept). Set SSR_ASSUME_YES=true to skip all prompts.
Extend the domain set in ~/.config/ssr/ssr.conf:
SSR_MACOS_PREF_DOMAINS_EXTRA="com.apple.universalaccess com.apple.Spotlight"
Mackup overlap: if you also configure mackup to sync
com.apple.dockor similar domains, two mechanisms will compete. Pick one.
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_SCAN_UNMANAGED_APPS |
true |
List .app bundles not from brew/MAS during sync |
SSR_APP_DIRS |
/Applications:~/Applications |
Colon-separated dirs scanned for unmanaged apps |
SSR_SNAPSHOT_MACOS_PREFS |
true |
Snapshot macOS pref domains during sync |
SSR_MACOS_PREF_DOMAINS |
(see below) | Full override of snapshotted domains (space-separated) |
SSR_MACOS_PREF_DOMAINS_EXTRA |
— | Extra domains appended to the default set |
SSR_PREFS_CONFLICT_DEFAULT |
accept |
Non-TTY conflict resolution: accept or reject |
SSR_ASSUME_YES |
false |
Auto-accept all pref-snapshot conflicts on restore |
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 |
Shell completions
ssr install offers to install completions automatically. To do it manually later:
ssr completions install # detects $SHELL
ssr completions install --shell all # zsh + bash + fish
ssr completions status # show what's wired up
ssr completions uninstall --shell all
Targets (user-scoped, no sudo):
| Shell | Path |
|---|---|
| zsh | ~/.zsh/completions/_ssr |
| bash | ~/.local/share/bash-completion/completions/ssr |
| fish | ~/.config/fish/completions/ssr.fish |
For zsh, ensure ~/.zsh/completions is on $fpath (the installer prints the snippet for ~/.zshrc if it isn't already). For bash, you need bash-completion@2 sourced from your shell rc — Homebrew install path is also printed. Fish needs no extra setup.
After installation, open a fresh shell or exec $SHELL and ssr <TAB> lists the available commands.
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-completions # install zsh/bash/fish completions
│ └── 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
│ ├── apps.sh # Detect unmanaged .app bundles (not brew/MAS)
│ ├── macos.sh # macOS-specific actions (Rosetta, defaults, etc.)
│ └── macos-prefs.sh # Snapshot/restore macOS pref domains
├── config/
│ ├── ssr.conf.example # Reference user config
│ └── macos-defaults.sh # Default macOS preferences
├── completions/
│ ├── _ssr # zsh
│ ├── ssr.bash # bash
│ └── ssr.fish # fish
├── 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.