My macOS dotfiles, managed with Dotbot.
git clone <repo_address> ~/.dotfiles
cd ~/.dotfiles
./installNew Mac: Homebrew may prompt you to install the Xcode Command Line Tools on the first run. That's expected and required; everything else runs on its own.
Apple Silicon only: Homebrew paths are hardcoded to
/opt/homebrew(inzprofile,zshrc, and the setup scripts). On an Intel Mac you'd need to point those at/usr/local.
./install does the following:
- Symlinks the config files into
~(creatingsecretsfrom the template first, if it's missing) - Creates
~/projects,~/work, and~/Documents/Snips(screenshot folder) - Installs everything in the Brewfile (packages and casks)
- Sets the Homebrew zsh as the default shell
- Installs the latest Node LTS via nvm
- Applies macOS system preferences
- Generates an SSH key if there isn't one
- Arms the git pre-push hook (see Testing)
Re-running it later is safe.
- Run
./install. - Add API keys to
~/.dotfiles/secrets. Install already creates this file (from the template, mode 600) and links it to~/.secrets, so it's ready to edit. - If you want a separate work git identity, create it from the template:
cp config/work-gitconfig.example ~/work/.gitconfig, then fill it in.
To have your keys in place from the very first shell, fill in secrets before running install (cp secrets.example secrets). Install never overwrites an existing secrets file.
Config files live in ~/.dotfiles/ and are symlinked into ~ by install.conf.yaml.
To add another one, add it under link::
~/.filename: filenameThen re-run ./install.
Anything private (API keys, tokens) goes in ~/.secrets, never in a tracked file.
If secrets doesn't exist, ./install creates it from secrets.example (mode 600) and links it to ~/.secrets. Add your values there:
$EDITOR ~/.dotfiles/secretszshrc sources ~/.secrets on startup. The file is gitignored and is never overwritten by re-running install.
The Brewfile holds all the Homebrew packages and apps. VSCode extensions are left out on purpose, since they sync on their own through VSCode Settings Sync.
Regenerate it from the current machine with:
brewbd # alias for: brew bundle dump --force --describe --no-vscodeHOMEBREW_BUNDLE_DUMP_NO_VSCODE=1 (set in zshrc) keeps extensions out even when running brew bundle dump directly. Commit the result.
setup_macos.zsh applies system preferences (Dock, Finder, keyboard, trackpad, screenshots, and so on).
./setup_macos.zsh --dry-run # show what would change
./setup_macos.zsh # apply itThese check that the repo still installs cleanly. The first three don't touch the machine, so they're safe to run anytime:
./lint.sh # syntax, shellcheck, YAML. Instant, no changes
./test/sandbox-install.sh # run the real ./install into a temp $HOME, twice
./test/brewfile-drift.sh # check every Brewfile package still resolves upstream
./verify.sh # check this machine against the expected setuptest/sandbox-install.shis the closest thing to a fresh-machine test without a fresh machine: it runs the real./installinto a temp$HOMEwith a wiped environment and every privileged step neutralised (dry-run), twice, asserting both the fresh-install result and idempotency.test/brewfile-drift.shcatches any packages renamed or dropped by brew. It runs automatically on a fresh-machine install; run it by hand anytime you want to check before provisioning.
Every setup script honours a dry-run mode that makes no changes:
DOTFILES_DRY_RUN=1 ./install # the whole install
./setup_macos.zsh --dry-run # or a single script- A pre-push hook runs
lint.shandtest/sandbox-install.shbefore everygit push, and blocks the push if either fails../installarms it automatically (by pointingcore.hooksPathat.githooks). Skip it for a single push withgit push --no-verify. - GitHub Actions runs the same two checks on every push. Nothing to set up.
- Brew drift
./installrunsbrewfile-drift.shautomatically the first time it installs Homebrew on a new machine (advisory — it never blocks the install), and never on re-runs. We can also manually trigger the drift check on demand from the repo's Actions tab before provisioning.
So once you've run ./install, every push is checked locally by the hook and on the server by CI. Running a script by hand is just for quicker feedback while editing.
After changing a dotfile, commit and push. On the other machine:
git pull
./install~/.gitconfig pulls in ~/work/.gitconfig for any repo under ~/work/, so work repos can use a different name and email. That file isn't in this repo (it's a separate identity); create it from the template:
cp config/work-gitconfig.example ~/work/.gitconfig
# then fill in your work name and emailBased on dotfiles.eieio.xyz