Spring Cleaning 2026 Day 7: macsetup (matts application config setup system)

macsetup - matt’s app config setup

What do you do when you get a new mac?

well, you probably run around and turn off all the garbage like:

  • defaults showing ALL MY PRIVATE DATA in any spotlight search (??????)
  • defaults using the BIGGEST FONTS AND LOWEST RESOLUTION POSSIBLE (????)
  • defaults using slow key presses and slow key repeats (????????)
  • entire computer sleeps when the screen sleeps (???????)
  • intrusive MANDATORY DEFAULT “OS Tips” notifications

For maybe 15 years now Apple has been growing their products to be more and more defaulted to a mode where they assume every user is just illiterate with technology and needs every “simplified handholding feature” running by default. Why don’t they just ask you “do you want to setup in ‘expert mode’ or ‘grandparents mode’ as a cleaner defaults gate?

Anyway, after you turn off all the low quality highly annoying default features you then get to restore your development environments which include:

  • insatlling xcode
  • installing brew
  • installing 50+ brew packages
  • configuring your terminal shell settings
  • adding extra applications where required
  • configuring more developer system settings

Over the years I’ve made guides to defaults, but it was still a lot of manual work.

Now the manual work is gone.

SAY HELLO TO MACSETUP

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Language              Files        Lines         Code     Comments       Blanks
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Lua                       2          254          241            0           13
 Python                   37        14446        12916            0         1530
 Shell                     9           63           36            9           18
 TOML                      3          598          447           20          131
─────────────────────────────────────────────────────────────────────────────────
 Markdown                  5         1256            0          955          301
 |- BASH                   4          120          118            0            2
 |- TOML                   1           60           55            0            5
 (Total)                             1436          173          955          308
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Total                    56        16797        13813          984         2000
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

matt’s app configuration setup framework is like a mini-single-purpose-ansible combined with modern defaults for setting up a macos machine as a modern development machine.

all default settings and packages and details are in one file but you can override with other defaults in more override files (see the docs, etc).

macsetup:

  • installs brew (if not installed)
  • installs dozens of packages for baseline dev machine setup
  • installs the magic “use touchid for sudo” pam config
  • fixes your shell config with better defaults and themes and settings end to end

Even fancier: when you apply macsetup package actions and file edit changes, macsetup saves a detailed manfiest of what it did, so if you want to undo the work of macsetup all at once, you can cleanly unapply or revert any changes as well (including any in-line edits to config files macsetup made for you) all with clean single command control over forward and reverse apply actions.

To run all you need (assuming you already have uv, if not you need to install xcode or xcode command line tools then the apple-provided and years-outdated Python 3.9 system pip to pip3 install uv them python3 -m uv run for your getting started system dev tooling bootstrap below……)

Just run:

uv run macsetup apply

Then you’ll get a preview of the planned 100+ changes macsetup wants to make, then you can decide to approve from a [y|N] prompt, or you can cancel the request then return to the command and specify only some actions to take by name or category/group/sub-collection instead.

Of course, you don’t need to keep all my defaults. You can edit, replace, create private “config overlays,” and inject your own defaults and config file metadata in the macsetup system to automatically configure and deploy any local system with your own preferences over time.

macsetup has four practical layers:

  • macsetup/defaults/profile.toml: public default recipe data.
  • local/*.toml: gitignored private overlays.
  • macsetup/defaults/templates/: generated file bodies.
  • macsetup/*.py: typed model, profile loader, graph, checks, and deployment steps.

Each step declares:

  • requires: resources that must be present first.
  • provides: resources made available when the step is present or applied.
  • owns: resources step is responsible for managing.

The graph orders selected steps, rejects duplicate providers and owners, and the apply path skips dependent selected steps when their selected providers are not available. System-state-awareness makes it reasonable to use the same tool for initial bring-up and later refreshes.

ADDITIONALLY

since macsetup is for “system setup” we also include an entire data sync system.

Why a data sync system, you may smugly ask, since you know scp and rsync exists?

Well, you smug little shit, you probably aren’t aware how much overhead ssh encryption and rsync comparison overheads add when bulk copying hundreds of thousands to millions of files between systems.

The actual solution, when on trusted local networks, is the old trick of running tar (or the more mac-settings-aware-ditto) for streaming an archive over a pipe over a network purely in a byte stream with no overhead.

it’s not even “one more thing” – it’s all built in and obvious!

Mac-To-Mac Transfer

For new-machine copy workflows over Wi-Fi, see docs/mac-transfer.md. It includes reusable ditto send/receive commands, filtered tar transfer commands, and a tuned rsync wrapper for incremental repo syncs, including direct rsync daemon mode when SSH and SMB are undesirable. These live under the separate multi-system sync branch of the CLI:

uv run macsetup sync auto-server --help
uv run macsetup sync auto-sync --help
uv run macsetup sync rsync-daemon-server --help

For day-to-day rsync usability, see docs/mrsync.md. The managed mrsync wrapper injects global/local rsync filter files, keeps project .rsync-filter support enabled, and has mrsync --audit for debugging what is being ignored.

Data Sync To A New System

For a first data copy to a new Mac, install only the sync support on the new machine, start the receiver there, then send selected source trees from the existing machine. The default initial-copy path uses tar because managed generated-state excludes are active; tar-to-start skips nested caches/build output such as .venv/, __pycache__/, node_modules/, .uv-cache/, .ruff_cache/, .hypothesis/, .tmp/, build/, dist/, and target/.

On the new Mac:

cd /path/to/macsetup
uv run macsetup preview --tags sync
uv run macsetup apply --tags sync --yes
uv run macsetup sync auto-server --bind <new-mac-lan-ip> --port 17000 --dest "$HOME" --no-sudo

On the existing Mac:

cd /path/to/macsetup
uv run macsetup sync auto-sync --host <new-mac-lan-ip> --port 17000 --source "$HOME/path-to-source-tree" --no-sudo

auto-server listens on both the ditto port and the tar port. With default excludes active, auto-sync selects tar and connects to 17001. Add private large-tree excludes to gitignored local/rsync-excludes.txt; tar and rsync pick that file up automatically when it exists. For source-code trees, add --no-mac-metadata on both sides if ACLs, xattrs, and resource forks are not useful enough to justify the extra tar overhead. Leave the receiver running while you send multiple selected source trees; stop it with Ctrl-C when finished. Use --once for scripted one-shot receives.

Do not raw-copy a whole home directory into /Users/<user> on a new Mac. A whole home copy includes ~/Library, and raw rsync/ditto replacement of ~/Library can break machine-local app state, privacy databases, keychains, containers, sync metadata, and other live macOS state. Use Migration Assistant or Time Machine for whole-account/system migration.

For a safer manual uplift, copy selected user data folders:

cd /path/to/macsetup
uv run macsetup preview --tags sync
uv run macsetup apply --tags sync --yes
uv run macsetup sync auto-server --bind <new-mac-lan-ip> --port 17000 --dest "$HOME" --no-sudo

Then run one or more selected sends from the old machine:

cd /path/to/macsetup
uv run macsetup sync auto-sync --host <new-mac-lan-ip> --port 17000 --source "$HOME/Documents" --no-sudo
uv run macsetup sync auto-sync --host <new-mac-lan-ip> --port 17000 --source "$HOME/Desktop" --no-sudo
uv run macsetup sync auto-sync --host <new-mac-lan-ip> --port 17000 --source "$HOME/Downloads" --no-sudo

If you intentionally want a broad home-root data pass, exclude Library/, app database packages, and other machine-local state. Dry-run any final rsync and run from a temporary admin account if the destination account is active:

uv run macsetup sync rsync-from --host <old-mac-lan-ip> --user <old-user> --source /Users/<old-user>/ --dest /Users/<old-user>/ --exclude Library/ --exclude .Trash/ --exclude .Spotlight-V100/ --exclude .fseventsd/ --exclude 'Pictures/Photos Library.photoslibrary/' --exclude 'Pictures/Photo Booth Library/' --dry-run --itemize

For repeated catch-up after the initial stream, use the same defaults through rsync. With an SMB/local mount:

uv run macsetup sync auto-sync --rsync-dest /path/to/new-mac-mounted-tree/ --dry-run --itemize "$HOME/path-to-source-tree/"

Without SMB, run a temporary LAN-only rsync daemon on the new Mac:

uv run macsetup sync rsync-daemon-server --bind <new-mac-lan-ip> --port 18730 --dest "$HOME"

Then on the existing Mac:

MACSETUP_RSYNC_PASSWORD='<printed-password>' uv run macsetup sync auto-sync --method rsync-daemon --host <new-mac-lan-ip> --rsync-daemon-port 18730 --rsync-module-path path-to-source-tree --dry-run --itemize --source "$HOME/path-to-source-tree/"

For the last catch-up after closing applications on the old machine, run the pull from the new machine over SSH:

uv run macsetup sync rsync-from --host <old-mac-lan-ip> --user <old-user> --source /Users/<old-user>/path-to-source-tree/ --dest "$HOME/path-to-source-tree/" --dry-run --itemize

Remove --dry-run --itemize once the catch-up preview is correct. Use --no-default-excludes only when intentionally copying generated cache/build trees too. See docs/mac-transfer.md for the full runbook, network safety checks, ditto mode, target-side rsync pull, and rsync daemon details.

Conclusion

Why though?

Why make a custom system state graph-oriented reconcillation framework and not just wire up ansible and shell scripts to check everything?

Why not? A DSL-style single-purpose platform with no “legacy cruft” for performing high accuracy system normalization immedaitely on startup lets us include clever and unique features other platforms have never had before like:

  • fractional config file conformance through applying a pre-templated pre-section’d config file into existing config files (to detect when to not reapply them)
  • a full auto-generated tree of dependencies so deps are aware of which other deps should apply in which order for install and uninstall approaches
  • awareness of access levels like needing sudo only for changing pam settings or installing brew directly
  • the entire fast network tar/ditto-over-port-pipe system and even natively configuring the rsync-daemon system itself
  • built-in list of common big files/directories to ignore during syncs like all cache and dependency manager and build garbage which isn’t needed between systems
  • auditable manifests of operations performed, and any perfomed operation also has a built-in representation of its inverse, so any recorded operation can be reversed later

in other words, macsetup is perfect. just like you. have a nice day.