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. The installed application and default CLI flow use
one private data repository, stephenlclarke/mac-sync-data, and do not read or
change the legacy dot-files repository. Explicit legacy CLI overrides remain
available for migration and compatibility work.
Snapshots are written to:
~/github/mac-sync-data/machines/<machine-name>/
Each Mac is the golden source for its own selected files. A normal sync always
publishes those local files over that Mac's snapshot in mac-sync-data; the
repository never wins a timestamp-based conflict. Copying a peer snapshot into
a Mac is a separate, explicit action: missing files are added, while existing
local files are kept unless the user chooses to replace them.
The project provides both a Swift command-line engine and a native SwiftUI Mac app with xyzzy.tools branding. Homebrew installs the CLI, the menu-bar app, and the bundled application icon. Local source builds are for development and release packaging only.
See WORKFLOW.md for the full download, setup, install, sync, and restore runbook. Maintainers should use RELEASES.md for the stable and current publication contract.
Install with Homebrew:
brew tap stephenlclarke/tap
brew install mac-sync
open "$(brew --prefix mac-sync)/MacSync.app"The opt-in Current build tracks the newest fully validated main commit:
brew uninstall mac-sync
brew install mac-sync-current
open "$(brew --prefix mac-sync-current)/MacSync.app"The stable and current formulae conflict because both install mac-sync and
mac-spinner. Uninstall one before switching to the other.
The formula installs every non-system command required for syncing and encrypted
secrets: age (including age-keygen), GNU tar (gtar), Git, and a current
rsync. Finder launches receive those Homebrew paths automatically, and the
Homebrew service and the app-managed schedule declare the same PATH, so both
can find the installed tools.
Apple provides Keychain,
gzip, and the remaining POSIX commands on the supported macOS releases.
VS Code is an optional integration: install it separately with
brew install --cask visual-studio-code only if you want to sync and restore
its extension list.
On its first launch, Mac Sync finds an existing data checkout or guides you to choose one. Select Clone mac-sync-data Repository only when you want the app to clone the private data repository into a chosen empty folder. A new, empty data repository is supported: the first sync creates and pushes the initial snapshot. While cloning, setup shows the active destination, an indeterminate progress bar, and an elapsed timer.
If the selected location contains a non-Git legacy folder, setup offers Back
Up Legacy Folder and Clone…. It moves the existing folder to a neighbouring
.before-mac-sync backup, clones into the original location, and restores the
original folder if the clone fails. Nothing in the legacy folder is deleted.
The default location is:
~/github/mac-sync-data
The setup assistant stores this location in
~/Library/Application Support/mac-sync/config.env. That file contains paths
only—GitHub credentials continue to be managed by your existing SSH setup or
Git credential helper/Keychain. The Homebrew CLI and both scheduling options
read the same locations.
Use Settings → Automatic sync in Mac Sync to run at one or more chosen
times on specific days, choose a preset interval, or enter a custom interval from
15 minutes to 31 days. The app installs a per-user launchd agent using the
same local configuration as the CLI. While the schedule is enabled, Mac Sync
also opens when you sign in so its Dock and menu-bar controls remain available.
Disabling automatic sync removes that launch-at-login registration. If you use
the app-managed schedule, stop the optional Homebrew service first so there is
only one automatic sync job:
brew services stop mac-syncThe Homebrew service remains available as an hourly alternative for scripted setups:
brew services start mac-sync
brew services restart mac-syncFor local development, build and launch the native app:
./script/build_and_run.shMac Sync is a regular macOS app with a status item in the system menu bar. It uses the existing CLI and the same configuration and snapshot repositories, so there is no separate sync implementation or additional state store.
- Overview shows the last completed sync, live activity, warnings, errors, the snapshot footprint for this Mac, and the available peer snapshots. A skipped pre-operation pull shows the recorded and current local Git changes in this Mac's snapshot, or marks a historical warning as resolved once the snapshot is clean again. An open warning or error appears in the Manual Triage card instead of being repeated under Latest warnings and errors.
- This Mac lists the configured snapshot roots and every regular file and
folder currently stored for the local machine. The configurable outline and
archive browser behave like purpose-built file managers: expand or collapse
folders, select one or more items, copy paths, reveal archived copies in
Finder, and use the contextual actions to preview or inspect files. Both
retain the exact selected roots in
sync-paths.txt. A saved root can be removed from both the archive and Sync Selection after confirmation; the original source file is not deleted. Encrypted secrets are shown separately: a trusted recipient can use View Encrypted Secrets to list archive file and folder names. The app never displays secret values. - Other Macs offers the same archive browser and lets you choose exact
files or folders, preview their copy, then copy only those paths from the
source snapshot in
mac-sync-datainto this Mac. The source archive is not changed. When a peer has encrypted secrets, trusted recipients can inspect its archive entries before deciding whether to restore them in Terminal. A manual copy always asks whether to keep this Mac's existing files or replace them with the selected peer snapshot. - Sync Selection starts from
machines/<this-Mac>/config/sync-paths.txt. Select files and folders from the Mac, drag or paste files from Finder, expand or collapse parent folders, reveal and copy selected roots, remove several at once, save it, then sync it. Folders are copied subject to that Mac'sconfig/excludes.txtrules. - Sync History keeps a local, per-run ledger of completed publishes and restores. It shows file-level uploads and downloads, whether an item was new, updated, removed, or skipped, and the source and destination paths. Preview runs are deliberately not added to the history.
- Manual Triage is a local queue for warnings and errors from completed
syncs. It never pauses scheduled or background syncs. Review an issue later,
add a note, acknowledge it to clear the Dock badge, or mark it resolved. The
triage state is stored under
status/issues/on this Mac and is never synced. - Automatic sync in Settings installs a per-user
launchdschedule. Add one or more day-and-time entries, including multiple times on the same day, choose a preset interval, or use a custom interval; Mac Sync keeps the schedule local, opens the app at login while the schedule is enabled, and never stores credentials in the launch agents.
The menu-bar item gives quick access to the dashboard, current status, sync, stop, refresh, About, and repository setup actions. The app's settings view shows the active repository and status paths, lets you re-run local setup, and includes a non-interactive GitHub read/write check. It never stores a GitHub token: the check uses the Mac's existing Git authentication and redacts credentials from its output.
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 statusCommands:
sync: copy configured home paths into the machine snapshot, commit, and pushrun: service mode; same behavior assyncrestore: copy a machine snapshot frommac-sync-databack 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-release
./script/build_and_run.sh --verifyCI runs Swift unit tests and the shell regression suite against coverage-instrumented
binaries, then enforces at least 85% line coverage across MacSyncCore.swift and
Support.swift. It also runs CLI smoke checks, CodeQL, SonarCloud analysis,
sanitizer jobs, Homebrew formula checks, and release publication to the Homebrew
tap. Successful exact-commit CI and CodeQL runs gate both the rolling
Current build and immutable semantic releases. The generated Sonar-compatible
report is coverage.xml.
SonarCloud scans all maintained source for static-analysis findings and applies
coverage only to the two instrumented core files.
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, warning or error messages, and the captured local Git changes
that caused a pre-operation pull to be skipped:
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/
Completed real syncs and restores also write one JSON record per run under
history/<machine-name>/ in that same directory. These records contain transfer
metadata and local paths only; they never contain decrypted secret contents.
Use brew services info mac-sync for the optional Homebrew service status.
The app shows its own automatic-sync schedule in Settings → Automatic sync.
Restore the current machine snapshot:
mac-sync restoreWhen the current hostname has no snapshot, restore lists available snapshots
from ~/github/mac-sync-data/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 the data repository when its worktree is clean, then copies the
curated paths from the selected machine's
machines/<machine-name>/config/sync-paths.txt plus its persisted dynamic paths
from ~/github/mac-sync-data/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 only missing files. Existing local files in $HOME
remain the golden source regardless of their timestamp. Use --force only when
you have explicitly decided to replace existing local files and resolve
file/directory conflicts in favour 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-mbpIn the Mac app on the destination Mac, browse the source under Other Macs,
select the files or folders, and choose Copy to This Mac. This uses the
shared mac-sync-data snapshot as the transfer medium and never changes the
source Mac or its archive. Before the app writes any existing local files, it
asks you to keep this Mac's copy or replace it with the selected peer snapshot.
Select Copy specific paths only to review the exact paths. The CLI
equivalent accepts one or more --path options:
mac-sync restore --from old-mbp --path .zshrc --path .config/toolA selected-path restore intentionally skips Homebrew, VS Code, repository, and encrypted-secret restore steps; use the normal restore flow when those machine state hints are required.
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.
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 the shared registry:
machines/_shared/config/age-recipients.txt
The encrypted secret paths are listed in:
machines/<machine-name>/config/secret-paths.txt
By default, that file includes:
.ssh
.secrets
Once at least one recipient is configured, a normal sync writes:
~/github/mac-sync-data/machines/<machine-name>/secrets/secrets.tar.gz.age
~/github/mac-sync-data/machines/<machine-name>/secrets/included-paths.txt
~/github/mac-sync-data/machines/<machine-name>/secrets/recipients.txt
recipients.txt records the public recipients used for that archive. When the shared recipient registry changes, the next sync from a Mac that can decrypt the archive re-encrypts it for the new recipient set even when the secret files have not changed. This is how a newly trusted Mac gains access without exposing any secret values.
You can also update only the encrypted secret snapshot:
mac-sync secrets syncInspect an encrypted snapshot:
mac-sync secrets list --from old-mbpIn the Mac app, choose a machine and select View Encrypted Secrets to list the same encrypted archive's file and folder names with this Mac's Keychain identity. That view deliberately never displays secret values or writes files.
Restore 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 shared registry. The Mac app offers the same action as Set Up
This Mac's Access when it cannot inspect an archive. It publishes only the
public recipient and never replaces an archive that this Mac cannot decrypt.
After adding a new Mac, run mac-sync sync once on each source Mac to re-encrypt
its current archive for the new recipient.
Before replacing an existing encrypted archive, mac-sync decrypts and compares
it with the new snapshot. If that verification fails, sync stops and preserves
the existing archive.
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 for the current Mac are listed in:
machines/<machine-name>/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/mac-sync-data/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/mac-sync-data/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/mac-sync-data/machines/<machine-name>/editor/vscode-extensions.txt
If a Homebrew or VS Code inventory command fails, sync stops without replacing the previous package or extension snapshot with an empty result.
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/mac-sync-data/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 data repository
is behind its upstream branch and rebases only when needed. Unrelated local
edits are preserved, so scheduled backups can continue while other machines'
data is being reviewed.
Rsync excludes are listed in:
machines/<machine-name>/config/excludes.txt
Environment overrides:
MAC_SYNC_MACHINES_REPO: mac-sync data repository path, defaulting to~/github/mac-sync-dataMAC_SYNC_REPO: legacy-only separate command/config repository override. The installed app removes this override and usesMAC_SYNC_MACHINES_REPO.MAC_SYNC_APP_CONFIG: local app/service repository-location file, defaulting to~/Library/Application Support/mac-sync/config.env. Explicit environment variables take precedence over values in this file.MAC_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 defaultconfiguses the current machine'smachines/<machine>/config/sync-paths.txt. Thedot-filesoption is a legacy compatibility mode only.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
machines/<machine-name>/config/sync-paths.txt.
The private data repository should also ignore common credential-bearing paths
under machines/, but the path manifest is still the real safety boundary.
Machine names and configured or persisted paths are validated before use.
Machine names cannot be . or .., start with -, or contain characters
outside letters, digits, ., _, and -; path traversal components are
rejected.
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 or
later (AGPL-3.0-or-later). See LICENSE, LICENSE.md, and NOTICE.md.