Skip to content

stephenlclarke/mac-sync

mac-sync

CI CodeQL Homebrew Quality Gate Status Coverage Bugs Code Smells Security Rating Maintainability Rating Duplicated Lines Lines of Code

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

Install with Homebrew:

brew tap stephenlclarke/tap
brew install mac-sync

Clone 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-files

Use Homebrew to manage the scheduled service:

brew services start mac-sync
brew services restart mac-sync
brew services stop mac-sync

For local development, build and run the Swift package directly:

make build
.build/debug/mac-sync --help

Usage

mac-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 status

Commands:

  • sync: copy configured home paths into the machine snapshot, commit, and push
  • run: service mode; same behavior as sync
  • restore: copy a machine snapshot from dot-files back into $HOME and re-clone missing GitHub repos
  • secrets: manage encrypted secret snapshots with age and Apple Keychain
  • packages: manage Homebrew snapshots, diffs, and installs
  • editor: manage VS Code extension snapshots, diffs, and installs
  • manifest: show configured and dynamically discovered backup paths
  • list: show every configured source path and repo destination
  • status: show repo, git, local status, and last-sync state
  • help [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.

Development

make ci
make package-release

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

Status

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 status

Sync 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

Restore the current machine snapshot:

mac-sync restore

When 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 --select

List available restore sources:

mac-sync restore --list-machines

Show restore help:

mac-sync help restore

Restore from another machine:

mac-sync restore --from old-mbp

Restore 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 --force

Preview a restore without changing local files:

MAC_SYNC_DRY_RUN=1 mac-sync restore --from old-mbp

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

Encrypted Secrets

Install the required tools:

brew install age gnu-tar

Initialize this Mac's encryption identity:

mac-sync secrets init

Show encrypted secrets help:

mac-sync help secrets

That 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 sync

Inspect an encrypted snapshot:

mac-sync secrets list --from old-mbp

Restore encrypted secrets:

mac-sync secrets restore --from old-mbp

Without --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 --force

Test Keychain and current-machine archive access:

mac-sync secrets test

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

Packages

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-sclarke

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

Editor State

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-mbp

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

Configuration

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 source

At 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-sync
  • MAC_SYNC_MACHINES_REPO: machine snapshot repo path, defaulting to ~/github/dot-files
  • MAC_SYNC_MACHINE: machine directory name, defaulting to the macOS host name
  • MAC_SYNC_STATUS_DIR: local status directory, defaulting to ~/Library/Application Support/mac-sync/status
  • MAC_SYNC_DRY_RUN=1: preview sync or restore changes without writing files, committing, or pushing
  • MAC_SYNC_DYNAMIC_REFS=0: disable dynamic dotfile reference discovery
  • MAC_SYNC_HOMEBREW=0: disable Homebrew package snapshotting and restore command suggestions
  • MAC_SYNC_VSCODE_EXTENSIONS=0: disable VS Code extension snapshotting and restore command suggestions
  • MAC_SYNC_GITHUB_ROOT: local GitHub clone root, defaulting to ~/github
  • MAC_SYNC_GITHUB_REPOS=0: disable GitHub repository snapshotting and restore cloning
  • MAC_SYNC_SECRETS=0: disable encrypted secret snapshotting and restore hints
  • MAC_SYNC_MANIFEST_SOURCE: choose config, auto, or dot-files. The default config uses config/sync-paths.txt. The dot-files option is retained only for older dot-files checkouts that still expose make print-mac-sync-paths.
  • MAC_SYNC_KEYCHAIN_SERVICE: Keychain service for the age identity, defaulting to mac-sync age identity
  • MAC_SYNC_KEYCHAIN_ACCOUNT: Keychain account for the age identity, defaulting to $USER or id -un
  • SCRIPT_COLOUR=off: disable colour output

Security Notes

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.

License

This repository is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). See LICENSE, LICENSE.md, and NOTICE.md.

About

mac-sync keeps a curated snapshot of important Mac dotfiles

Resources

License

AGPL-3.0, Unknown licenses found

Licenses found

AGPL-3.0
LICENSE
Unknown
LICENSE.md

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors