Personal macOS dotfiles plus a one-shot setup script for a fresh Mac. Opinionated and single-user, not built to be configurable.
Irreversible. Read scripts/install.sh before running it.
sudo rm -rfon/Applications/{GarageBand,Pages,Numbers,Keynote}.app- Copies every file in
meta/manifest.csvover its system path, no backup of what was there - Runs every
defaults writeline inmeta/macOS_settings.sh, thenkillall Dock Finder SystemUIServer - Installs Homebrew (which brings the Xcode Command Line Tools), Oh My Zsh and the
omz-git-branchplugin brew update && brew upgrade(upgrades packages you already had), installsconfig/Brewfile, thenbrew cleanup- Unless
~/.ssh/id_ed25519exists,gh auth login(browser) generates an SSH key and adds it to your GitHub account chsh -s /bin/zsh- Offers
sudo softwareupdate -ia --restartat the end
Test it first, see Test locally and Test on CI.
./scripts/install.sh --dry-run # print commands, change nothing
./scripts/install.sh # run every step not yet done
./scripts/install.sh --skip ssh # leave steps for later, a plain rerun picks them up
./scripts/install.sh --only ssh,dotfiles # run just these, even if already done
./scripts/install.sh --fresh # forget progress and run every step againSteps, in order: homebrew packages omz macos dotfiles ssh shell update. Each finished step is recorded in
~/.local/state/dots/install-done, so a run that fails or is stopped with Ctrl+C resumes at the step it stopped on.
--dry-run reads that file but never writes it.
Every run prompts for confirmation. The packages step opens the App Store and blocks until you confirm you are
signed in, since the Brewfile has mas entries, even under --dry-run. If brew bundle can't install an item (eg a
cask Homebrew has since disabled), the install skips it, carries on, and lists it at the end; fix it and rerun with
--only packages.
From a Mac with nothing checked out (installs the Xcode Command Line Tools if missing, clones to ~/Downloads/dots,
deleting any existing copy, then runs the installer):
bash -c "$(curl -fsSL https://raw.githubusercontent.com/cpwillis/dots/main/scripts/repo_download.sh)"tests/sandbox.sh runs repo_download.sh, install.sh and update_configs.sh against a throwaway home, with
brew, sudo, defaults and the other system commands stubbed out, so nothing on your Mac changes. It covers
resuming after a failure or Ctrl+C, --skip/--only, skipped Brewfile items and the sync's commit guards, in about
20 seconds. macOS only, needs network. It exits non-zero and keeps the sandbox if any check fails.
./tests/sandbox.sh.github/workflows/install-test.yml ("Fresh macOS Install" in the Actions tab) runs install.sh end to end on a
GitHub macOS runner. It only runs when dispatched by hand, one run at a time:
gh workflow run install-test.yml
gh run watchFirst it makes the runner look like a fresh Mac: it deletes the runner's Homebrew and its preinstalled Chrome,
Firefox and VS Code, and points xcode-select at the Command Line Tools. It skips App Store apps via
HOMEBREW_BUNDLE_MAS_SKIP (no Apple ID) and pre-creates ~/.ssh/id_ed25519 so the gh login is skipped. After the
install it checks that a rerun has nothing to do and that a new login shell, with a clean environment as Terminal
opens it, starts without errors.
Not covered: repo_download.sh and the Command Line Tools install (the runner already has them), App Store apps, and
the gh SSH key login.
scripts/update_configs.sh pulls live config into the repo, overwriting:
- every
repo_pathinmeta/manifest.csv, from its system path config/Brewfile, viabrew bundle dump --force; if the dump has nomaslines it reinstallsmasand retries, up to 3 attempts
It then rewrites email and signingkey in config/.gitconfig to placeholders, so real values never reach the repo.
--commit refuses to commit if any file under config/ or meta/ contains your $HOME path.
./scripts/update_configs.sh # sync only
./scripts/update_configs.sh --commit # sync, then git add . && git commit && git pushscripts/generate_macOS_settings.sh regenerates meta/macOS_settings.sh from the current system, overwriting it.
Run it yourself, update_configs.sh does not call it. Keys missing from the defaults database are skipped silently,
which is why some comments in that file have no command under them.
Add a row to meta/manifest.csv: name, repo_path, system_path. Install and sync both read it, so one row covers
deploy and sync back. system_path is eval'd, so $HOME expands.
config/git-hooks/pre-push blocks direct pushes to main (bypass with ALLOW_MAIN_PUSH=true). Nothing deploys it.
Copy it into a repo's .git/hooks/ and chmod +x it.
config/gnupg/gpg-agent.conf is deployed, but GPG keys and ~/.aws are not in this repo. Restore those by hand.
Set email and signingkey in ~/.gitconfig. It deploys with placeholders and gpgSign = true, so commits fail
until both are set.
Restore from backups in ~/Documents/Misc:
- iTerm2: Preferences → General → Settings → Import All Settings and Data
- Alfred: Advanced → Set preferences folder
Install by hand: KiCad, CleanMyMac, PrusaSlicer.
Re-enter licenses: Alfred, Shottr, BetterDisplay, Bruno, CleanMyMac, TablePlus.