mac-sync keeps a curated snapshot of important Mac dotfiles, Homebrew
packages, VS Code extensions, encrypted secrets, and local GitHub clones in
git, split by machine name. This repo owns the command and backup/restore
configuration; machine snapshots live in the separate
stephenlclarke/dot-files repo.
Snapshots are written to:
~/github/dot-files/machines/<machine-name>/
The command is implemented as a SwiftPM package and distributed as a Homebrew-managed Swift binary. Local source builds are for development and release packaging only.
See WORKFLOW.md for the full download, setup, install, sync, and restore runbook.
Install with Homebrew:
brew tap stephenlclarke/tap
brew install mac-syncClone the config repo and machine snapshot repo at their default paths:
git clone https://github.com/stephenlclarke/mac-sync ~/github/mac-sync
git clone https://github.com/stephenlclarke/dot-files ~/github/dot-filesUse Homebrew to manage the scheduled service:
brew services start mac-sync
brew services restart mac-sync
brew services stop mac-syncFor local development, build and run the Swift package directly:
make build
.build/debug/mac-sync --helpmac-sync sync
mac-sync restore
mac-sync restore --select
mac-sync restore --list-machines
mac-sync restore --from old-mbp
mac-sync secrets init
mac-sync packages diff --from old-mbp
mac-sync packages install --from old-mbp
mac-sync editor diff --from old-mbp
mac-sync editor install --from old-mbp
mac-sync secrets list --from old-mbp
mac-sync secrets restore --from old-mbp
mac-sync manifest list
mac-sync help restore
mac-sync help secrets
mac-sync list
mac-sync statusCommands:
sync: copy configured home paths into the machine snapshot, commit, and pushrun: service mode; same behavior assyncrestore: copy a machine snapshot fromdot-filesback into$HOMEand re-clone missing GitHub repossecrets: manage encrypted secret snapshots withageand Apple Keychainpackages: manage Homebrew snapshots, diffs, and installseditor: manage VS Code extension snapshots, diffs, and installsmanifest: show configured and dynamically discovered backup pathslist: show every configured source path and repo destinationstatus: show repo, git, local status, and last-sync statehelp [topic]: show general help or command-specific help
During sync, in-progress work is shown with a compact three-dot figure-eight Braille
marker from mac-spinner, and completed work is printed with a tick marker.
Paths that are already unchanged stay quiet.
make ci
make package-releaseCI runs Swift unit tests with coverage, the existing shell regression suite, CLI smoke checks, CodeQL, SonarCloud analysis, sanitizer jobs, Homebrew formula syntax checks, and branch prebuilt publishing for the Homebrew tap.
Show the current mac-sync version SHA, local repo paths, local status files,
the last completed sync, the amount of data changed by that sync, total machine
snapshot storage, and warning or error messages captured during the last sync:
mac-sync statusSync status is local machine state and is intentionally not committed to the repo. By default it is written under:
~/Library/Application Support/mac-sync/status/
Use brew services info mac-sync for the Homebrew service status.
Restore the current machine snapshot:
mac-sync restoreWhen the current hostname has no snapshot, restore lists available snapshots
from ~/github/dot-files/machines/ and prompts for the source machine. Use
--select to force that prompt even when the current hostname exists:
mac-sync restore --selectList available restore sources:
mac-sync restore --list-machinesShow restore help:
mac-sync help restoreRestore from another machine:
mac-sync restore --from old-mbpRestore pulls both repos first when their worktrees are clean, then copies the
curated paths from config/sync-paths.txt plus the selected machine's persisted
dynamic paths from ~/github/dot-files/machines/<machine-name>/dynamic-sync-paths.txt.
It also compares the selected machine's Homebrew snapshot with the local
Homebrew and VS Code extension state and re-clones missing GitHub repositories
from the selected machine's saved clone list into ~/github.
By default, restore copies missing files and files that are newer in the repo
snapshot while keeping newer local files in $HOME. Use --force to overwrite
newer local files and resolve file/directory conflicts in favor of the snapshot:
mac-sync restore --from old-mbp --forcePreview a restore without changing local files:
MAC_SYNC_DRY_RUN=1 mac-sync restore --from old-mbpWhen Homebrew packages differ, restore prints the manual commands needed to install missing taps, formulae, and casks or upgrade outdated packages from the synced list. It does not run those commands for you and it does not uninstall extra local packages.
When VS Code extensions differ, restore prints the manual code commands needed
to reconcile the local extension set. It does not run those commands for you.
If an encrypted secrets snapshot exists for the selected machine, restore prints
the mac-sync secrets list and mac-sync secrets restore commands to inspect
or restore it. Normal restore never decrypts secrets automatically.
GitHub clone restore is conservative: it only uses real GitHub remotes captured from git worktrees under the configured GitHub root, strips credentials from stored remote URLs, and skips any target path that already exists.
Install the required tools:
brew install age gnu-tarInitialize this Mac's encryption identity:
mac-sync secrets initShow encrypted secrets help:
mac-sync help secretsThat command creates an age identity if needed, stores the private identity in
Apple Keychain under mac-sync age identity, and adds only the public recipient
to:
config/age-recipients.txt
The encrypted secret paths are listed in:
config/secret-paths.txt
By default, that file includes:
.ssh
.secrets
Once at least one recipient is configured, hourly sync writes:
~/github/dot-files/machines/<machine-name>/secrets/secrets.tar.gz.age
~/github/dot-files/machines/<machine-name>/secrets/included-paths.txt
You can also update only the encrypted secret snapshot:
mac-sync secrets syncInspect an encrypted snapshot:
mac-sync secrets list --from old-mbpRestore encrypted secrets:
mac-sync secrets restore --from old-mbpWithout --force, restore refuses to overwrite existing local secret files.
Use --force to overwrite files from the encrypted snapshot:
mac-sync secrets restore --from old-mbp --forceTest Keychain and current-machine archive access:
mac-sync secrets testEach trusted Mac should run mac-sync secrets init, which adds that Mac's public
recipient to the repo. Future encrypted snapshots are encrypted to every
recipient in config/age-recipients.txt, so any matching Keychain identity can
decrypt them.
Homebrew package state is captured during normal mac-sync sync when brew is
available. Manage it directly with:
mac-sync packages sync
mac-sync packages diff --from old-mbp
mac-sync packages install --from old-mbp
mac-sync packages install --from old-mbp --formulae-only
mac-sync packages install --from old-mbp --admin-user adm-sclarkepackages diff prints the same manual commands that restore prints.
packages install runs brew bundle install from the selected machine
snapshot. Use --formulae-only to skip casks, or --admin-user when cask
installs need a different admin-capable account.
VS Code extension state is captured during normal mac-sync sync when the
code CLI is available. Manage it directly with:
mac-sync editor sync
mac-sync editor diff --from old-mbp
mac-sync editor install --from old-mbpeditor diff prints the manual code --install-extension and
code --uninstall-extension commands. editor install reconciles the local VS
Code extensions to the selected machine snapshot.
The configured backup paths are listed in:
config/sync-paths.txt
Paths are relative to $HOME unless they start with /. Inspect the active
manifest with:
mac-sync manifest list
mac-sync manifest configured
mac-sync manifest dynamic
mac-sync manifest sourceAt runtime, mac-sync also scans safe top-level dotfiles in $HOME and follows
safe $HOME, ${HOME}, and ~ references it finds. This keeps sourced files
such as ~/.shellenv, ~/.aliases, ~/.functions, and referenced plugin
directories in the sync set without hand-editing the manifest every time a
startup file changes. The generated per-machine dynamic list is persisted to:
~/github/dot-files/machines/<machine-name>/dynamic-sync-paths.txt
On later runs, paths that were previously dynamic but are no longer discovered are pruned from that machine snapshot, unless they overlap a curated path in the configured manifest.
Homebrew package state is captured during sync when brew is available. The
generated per-machine lists are persisted to:
~/github/dot-files/machines/<machine-name>/homebrew/
That directory contains sorted taps.txt, formulae.txt, and casks.txt
lists, plus a generated Brewfile for browsing or reuse.
VS Code extension state is captured during sync when code is available. The
generated per-machine manifest is persisted to:
~/github/dot-files/machines/<machine-name>/editor/vscode-extensions.txt
Git repositories under ~/github, including nested paths such as
xyzzy.tools/fixdecoder_rs, are captured during sync when they have at least
one GitHub remote. The generated per-machine clone list is persisted to:
~/github/dot-files/machines/<machine-name>/github-repositories/repositories.txt
Each row stores the path relative to ~/github and a credential-free GitHub
clone URL. Non-GitHub remotes, submodules, and non-repo directories are ignored.
Before pushing a machine snapshot, mac-sync checks whether the machines repo
is behind its upstream branch and rebases only when needed. Unrelated local
edits elsewhere in the dot-files checkout are preserved, so hourly backups can
continue while files outside machines/<machine-name> are being edited.
Rsync excludes are listed in:
config/excludes.txt
Environment overrides:
MAC_SYNC_REPO: mac-sync command/config repo path, defaulting to~/github/mac-syncMAC_SYNC_MACHINES_REPO: machine snapshot repo path, defaulting to~/github/dot-filesMAC_SYNC_MACHINE: machine directory name, defaulting to the macOS host nameMAC_SYNC_STATUS_DIR: local status directory, defaulting to~/Library/Application Support/mac-sync/statusMAC_SYNC_DRY_RUN=1: preview sync or restore changes without writing files, committing, or pushingMAC_SYNC_DYNAMIC_REFS=0: disable dynamic dotfile reference discoveryMAC_SYNC_HOMEBREW=0: disable Homebrew package snapshotting and restore command suggestionsMAC_SYNC_VSCODE_EXTENSIONS=0: disable VS Code extension snapshotting and restore command suggestionsMAC_SYNC_GITHUB_ROOT: local GitHub clone root, defaulting to~/githubMAC_SYNC_GITHUB_REPOS=0: disable GitHub repository snapshotting and restore cloningMAC_SYNC_SECRETS=0: disable encrypted secret snapshotting and restore hintsMAC_SYNC_MANIFEST_SOURCE: chooseconfig,auto, ordot-files. The defaultconfigusesconfig/sync-paths.txt. Thedot-filesoption is retained only for older dot-files checkouts that still exposemake print-mac-sync-paths.MAC_SYNC_KEYCHAIN_SERVICE: Keychain service for theageidentity, defaulting tomac-sync age identityMAC_SYNC_KEYCHAIN_ACCOUNT: Keychain account for theageidentity, defaulting to$USERorid -unSCRIPT_COLOUR=off: disable colour output
The regular dotfile sync list is explicit by design. Do not add raw secret
material such as SSH private keys, cloud credentials, token files, shell
history, or decrypted secret directories to config/sync-paths.txt.
The machine snapshot repo should also ignore common credential-bearing paths
under machines/, but the path manifest is still the real safety boundary.
Use encrypted secrets for ~/.ssh, ~/.secrets, or similar sensitive paths.
Only encrypted *.age snapshots and public recipients belong in git. The
private age identity must stay in Apple Keychain or another secret manager.
This repository is licensed under the GNU Affero General Public License v3.0
(AGPL-3.0). See LICENSE, LICENSE.md, and NOTICE.md.