Config files for macOS. Provisioned declaratively with mise bootstrap: the repo-root mise.toml describes the packages, dotfile symlinks, login shell and macOS defaults for the whole machine, and config/mise/config.toml has the global mise settings and tools.
This is one person's setup, so fork it instead of running the install script as it is. It needs an Apple Silicon Mac, because /opt/homebrew is hardcoded.
Change in your fork:
install.shand this README: point theiain/dotfilesURLs at your fork.config/git/allowed_signers: delete my keys, or your machine keeps trusting them. Your own key is added during setup.config/direnv/direnv.toml: replace~/iainin the trusted prefixes.claude/settings.json: review it, as it uses Claude's auto permission mode and turns on my plugins and remote control.config/iterm/: delete it, or replace my hostnames and home path.
Expect these changes to your machine, and preview them with mise bootstrap -n:
- ⌘Space no longer opens Spotlight, and macOS's ⌘⇧3, ⌘⇧4 and ⌘⇧5 screenshot shortcuts are off in favour of macshot. Nothing here installs a replacement for Spotlight.
- Desktop icons are hidden, Finder shows hidden files, and Finder and the Dock restart on every bootstrap.
- Root-owned files are installed: Touch ID for
sudo, and a pf rule with a LaunchDaemon that forwards local port 443 to pitchfork. - fish becomes your login shell.
DOCKER_HOSTpoints at Colima, so tools that honour it stop using Docker Desktop.- Caches and tool data move to the XDG folders, so npm, Go, cargo and Maven download their files again.
--force-dotfilesreplaces any existing dotfiles that conflict, so back up yours first.mise bootstrap packages pruneremoves Homebrew packages not declared here.
Released under the MIT License.
In Terminal.app:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/iain/dotfiles/main/install.sh)"Have your GitHub password and 2FA at hand: no password manager is installed yet, and the GitHub login opens in Safari. install.sh does what the bootstrap can't do for itself, then starts it:
- Names the machine. The status line, Claude's machine context and the signing key's title use it, and a new Mac comes with a name like
Iains-MacBook-Pro. - Installs Homebrew, interactively: it asks for your password, and brings the Command Line Tools and with them
git, which mise needs to clone this repo. - Logs
ghin to GitHub over SSH. Let it generate and upload a key: the bootstrap clones~/Code/spectralover SSH, and mise gets its GitHub token fromgh(without one, installing tools runs into the 60/hour anonymous API limit). - Adds that key to GitHub again, as a signing key. GitHub keeps authentication and signing keys apart.
- Installs mise from
mise.run(a brewed mise can'tmise self-update) and bootstraps from this repo, cloned to~/Code/dotfiles.
Every step is skipped when already done, so after a failure, run it again: it bootstraps from the existing checkout. Run it as above rather than through curl | bash, so its prompts can read the terminal.
In order, bootstrap:
- Installs Homebrew if missing, then the
[bootstrap.packages](formulae + casks/fonts), and copies the root-owned system files: Touch ID forsudo, and pitchfork's port forward. - Symlinks dotfiles (
config/*→~/.config/*,rc/*→~/.<name>,claude/*→~/.claude/*,bin/*→~/.local/bin/*).symlink-eachlinks each file individually, so machine-local files like~/.config/git/config.localare left untouched.config.local.exampleis excluded, as it's documentation. - Writes macOS defaults.
- Sets fish as the login shell.
- Installs the pinned tools (
ruby,node,hk, …). mise reloads config after step 2, so on a fresh machine this already sees the just-linked~/.config/mise/config.toml, and one run is enough. - Runs the
bootstraptask: per-machine setup that isn't declarative, then a read-only commit-signing diagnostic. It asks for your git identity, suggesting the name and email of your GitHub account, and writes it to~/.config/git/config.local; if it couldn't prompt, runmise run setup-identity. It also adds this machine's signing key toconfig/git/allowed_signers, and sets up the Claude plugins.
Re-run the bootstrap from the checkout any time; anything already in its desired state is skipped:
cd ~/Code/dotfiles
mise bootstrap -n # preview — changes nothing
mise bootstrap # apply
mise bootstrap status # drift from the declared stateThe machine bootstrap is in the repo-root
mise.toml, a project config, on purpose. mise merges[bootstrap]across the config hierarchy like[tools], so if it lived in the global config, runningmise bootstrapin any project would also re-apply these packages, dotfiles, and macOS defaults.
From here on, work in Ghostty, where fish is now the login shell. Set Firefox as the default browser, and grant the permissions apps ask for on first launch, such as Screen Recording for macshot. Vim installs its plugins the first time it starts.
The bootstrap added this machine's key to config/git/allowed_signers, so its commits verify locally. Commit that change, which also tests signing:
cd ~/Code/dotfiles
git commit -am "Allow Signing Key for $(scutil --get LocalHostName)" && git log --show-signature -1mise run check-signing re-runs the diagnostic and prints the exact fix for anything still missing.
config/
fish/config.fish — shell: PATH, aliases, abbreviations
git/ — git config, global ignore, diff attributes, allowed signers
ghostty/config — terminal appearance and keybinds
mise/config.toml — global mise settings, XDG + tool storage env vars, pinned tools (languages, jdx's CLIs, kubectl, uv, yq)
starship.toml — prompt theme (single-line, Nerd Font icons)
vim/ — modular vim config (auto-sourced via glob)
rc/
vimrc — vim entry point (sources files from config/vim/)
bin/
dotfiles-setup — per-machine setup driven by mise tasks (identity, Claude, signing)
pitchfork-supervisor — starts the pitchfork supervisor as the user for its LaunchAgent
claude/
settings.json — enabled plugins + marketplaces, hooks, permissions
system/ — root-owned files copied (not symlinked) by mise bootstrap
etc/pam.d/sudo_local — Touch ID for sudo
etc/pf.anchors/dotfiles-pitchfork — forwards loopback 443 to pitchfork's proxy on 8443
Library/LaunchDaemons/dev.dotfiles.pitchfork-pf.plist — loads that rule at boot
install.sh — first thing on a new Mac: machine name, Homebrew, gh and its keys, mise, bootstrap. Outside bin/, so it isn't linked onto PATH
mise.toml — the mise bootstrap config (packages, system files, dotfiles, login shell, macOS defaults, LaunchAgents, setup tasks)
Bootstrap is declarative and idempotent, so day-to-day changes are edits to mise.toml and the files under config/, rc/, etc. Run these from the repo:
mise bootstrap status # what's drifted from the declared state
mise dotfiles apply -n # preview dotfile symlink changes
mise run setup-identity # (re)seed git identity / ssh / fish-local
mise run check-signing # audit the commit-signing chain
mise doctor project # check drift, GitHub login, signing and pitchforkmise bootstrap only installs what's missing; it never upgrades. One task does all the upkeep:
mise run upgrade -n # preview — changes nothing
mise run upgradeIn order, it:
- Upgrades mise itself, then the bootstrap packages, everything else Homebrew manages (Homebrew itself, formulae a project installed, casks from before the bootstrap), the mise tools and the uv tools.
- Removes what's no longer used: tool versions no config asks for (
mise prune), and old Homebrew versions and downloads (brew cleanup). - Re-runs the Claude setup, so a new herdr's hooks reach Claude, then updates the Claude plugins of both profiles (restart Claude to load them) and the vim plugins.
- Relinks any Homebrew formula a failed upgrade left unlinked, which once took
ghoffPATH. - Lists pending macOS updates. It doesn't install them, as they can need a restart: use System Settings or
softwareupdate --install.
mise bootstrap packages upgrade only upgrades what mise.toml declares, and leaves casks that Homebrew installed before the switch to bootstrap alone, so brew upgrade follows it for the rest and updates Homebrew itself. Casks that update themselves, like macshot, are left to their own updater.