The system
What chezmoi is, and why you run it.
chezmoi keeps the configuration for your environment in one Git repository, and lays those files down in the right places on each supported profile: full Mac desktop, Linux/WSL terminal layer, or headless container. Understanding it means you can explain, reproduce, and repair the setup instead of treating it as hidden magic.
The mental model
There are two sides. On one side is a source repository — a folder full of your config files, tracked in Git. On the other is your home directory, where programs actually look for their settings. chezmoi's whole job is to copy from the first to the second, correctly.
Files in the repo start with dot_ instead of a dot so they're visible and easy to work with. A .tmpl ending means the file has blanks chezmoi fills in per machine — your name, your email, whether it's a work computer. That's how one repo serves many people without hard-coding anyone.
What apply actually does
When you run chezmoi apply, it walks the source repo and, for each file, decides what your home directory should look like, then makes it match. Concretely it:
- fills in any templates with this machine's answers,
- writes or updates the real config files in your home directory,
- runs setup scripts that should run — installing software, setting macOS preferences — and skips the ones that already ran.
It only changes what's different. Run it twice in a row and the second run does almost nothing — that's the point. It's safe to run any time you want to be sure your machine or shell profile matches the repo.
Why it's in version control
Because your setup is code. It lives in Git, so every change is recorded, you can see what changed and when, and you can roll back. Share the repo and someone else gets the same operating model: machine setup, terminal workflow, and agent instructions. Lose the laptop and a new one is minutes away, not days.
For files managed by chezmoi, the repo is the source of truth: chezmoi diff shows what would change, and chezmoi apply puts managed files back to the repo version. That is powerful, but it also means this setup intentionally changes the Mac.
chezmoi edit <file> | Edit a managed file at its source instead of changing the generated copy that the next apply will replace. |
chezmoi cd | Open a shell in the Groundwork source checkout when you want to inspect or work on the setup as a repository. |
First-time setup
This is not a passive app installer. It changes shell, Git, terminal multiplexer, editor, Homebrew, Finder, keyboard repeat, browser/developer, and app settings. First apply creates a focused backup at ~/.local/state/groundwork/backups/latest. Restore the backed-up files and macOS defaults with groundwork-restore. Already-used Macs can also choose an optional clean start that moves old Neovim, tmux, and zsh completion state to ~/.local/state/groundwork/reset/latest.
On a fresh Mac, run the bootstrap script. It installs the Apple/Homebrew/GitHub/chezmoi prerequisites, clones or updates github.com/jasondockery/groundwork at ${GROUNDWORK_DIR:-~/code/groundwork}, then applies it with chezmoi. The script is the source of truth, so the docs do not duplicate every internal command. On Windows, first install WSL2/Ubuntu and run the Unix shell path inside WSL rather than trying to apply the macOS bootstrap in PowerShell.
The repo is a visible project checkout; .chezmoiroot keeps only home/ mapped into your actual home folder.
After setup, run groundwork-docs to open the complete local site from that active checkout. Do not bookmark a guessed file:// location; older direct chezmoi installs and the current visible-checkout bootstrap use different source layouts.
Fresh setup also asks which terminal multiplexer to manage: Herdr (recommended), tmux, both with Herdr primary, or None. The prompt links Herdr's Apache-2.0 license so a managed environment can choose deliberately. Existing installations missing this newer answer are migrated to tmux without a surprise prompt or behavior change; review it later with groundwork-configure.
Use the one-liner:
$ bash -c "$(curl -fsSL https://raw.githubusercontent.com/jasondockery/groundwork/main/bootstrap-mac.sh)"
To read it first, download it to /tmp/bootstrap-mac.sh, open it with less, then run bash /tmp/bootstrap-mac.sh.
The first run asks only for the facts and explicit tool choices needed to build a usable machine baseline — your name and email, whether this is a work machine, whether a Mac is a server/CI machine, which password manager and terminal multiplexer you want, and whether to install optional large toolchains such as Unity or Xcode. Your answers are saved, so it won't ask again. From there it installs the selected baseline and configures the machine.
Groundwork starts with the role derived from the bootstrap facts and on stable application channels. Environment role, release posture, and the optional Xcode beta channel are preferences you can revisit, so bootstrap does not ask for them. Run groundwork-configure later; Xcode beta appears only when Xcode is enabled, while groundwork-profile set preview is the focused release-posture shortcut. Accepting applies the selected desired state immediately, and groundwork-doctor --browser reports the stored posture and Xcode states.
Two prompt styles appear during setup, because two tools ask. The setup questions are chezmoi's: a yes/no question ends with its accepted answers and its default spelled out — type y or t for yes, n or f for no, and a bare Enter keeps the named default (the dim bool, default false under the cursor is chezmoi's type hint, not something to type). Groundwork's own prompts (bootstrap, repair offers) use the shell convention instead: a bracketed affordance like [y/N] or [KEEP/switch/login], where the capitalized choice is the default and Enter takes it. In both styles, a run with no terminal attached takes the default and says so rather than hanging.
The backup is scoped to what this repo can reasonably restore: selected managed config files, selected macOS defaults keys, and Homebrew state notes. It does not uninstall apps, undo Homebrew upgrades, reverse manual permissions, or replace Time Machine.
If the Mac already has an old dev setup, answer yes to the reset prompt only when you want a cleaner start. It moves common conflict zones aside — Neovim config/state/cache, tmux legacy config/plugin/restore state, and zsh completion dumps — before applying this repo. It does not delete them.
Don't hand-edit a file that chezmoi manages (like ~/.zshrc) — the next apply overwrites it. Change it in the repo instead, commit, then apply. The repo always wins, by design.
Customize without forking
Groundwork is a shared base with a personal layer. For normal personal preferences, use the local override files that Groundwork intentionally loads last: ~/.zshrc.local, ~/.gitconfig.local, and ~/.config/tmux/tmux.local.conf. For setup choices (your profile, work machine, password manager, and so on), run groundwork-configure — it shows your current answers, changes only what you pick, previews the diff, and applies after you confirm. (Plain chezmoi init only regenerates from your existing answers; it never asks what to change.) See Customizing for the full tiered model.
Groundwork treats Docker as a way to run the environment, not as a standalone lesson. Linux and WSL2 get the shared terminal layer: shell, Git, tmux, Neovim, mise/uv, helper scripts, and agent instructions. macOS GUI apps, App Store apps, browser helpers, and macOS defaults are skipped when .chezmoi.os is not darwin; on macOS, the server/CI prompt lets a headless Mac skip the same desktop layer.
The Docker image is the non-interactive headless profile for agents, CI, and disposable shells. From the repo root, build it with docker build -t groundwork . and run docker run -it --rm groundwork. Keep that groundwork tag for the image you actually use; when you or an agent rebuild only to test a Dockerfile change, build it with groundwork-docker-build-scratch <purpose> . instead — the wrapper applies the ephemeral labels and the groundwork/scratch:<purpose> tag so groundwork-docker-tidy can remove it later — see Platforms for the contract. Personal Macs can use Docker Desktop; work Macs use Colima plus the Docker CLI, so start the daemon with colima start first. This does not try to teach Dockerfiles, Compose, or app hosting; it just gives the Groundwork terminal environment somewhere reproducible to run. See Platforms for the profile-by-profile split.
Day to day
When the shared repo gets new changes, pull them and apply:
$ chezmoi update --init
# pulls the latest repo and applies it in one step
# --init also refreshes the setup answers when new questions were added:
# every answer you already gave is kept, only the new questions are asked
That refreshes your config from the repo. If the update changes an install hook, such as the Brewfile bundle, that hook can add newly declared tools. It does not request routine upgrades of already-installed Homebrew apps, casks, or mise runtimes; Homebrew may still update a dependency when installing a new package requires it. To freshen everything at once, the setup ships an update-all command:
$ update-all
# configuration + upgrades + maintenance + a consent-gated macOS health postflight
Groundwork does not install unattended Homebrew or mise upgrade jobs. Run update-all intentionally and skim its consolidated receipt. One machine-wide lock prevents terminals or agents from racing the same update. Groundwork synchronizes first, hands the invocation to the freshly applied runner, inventories every Homebrew formula and cask, and verifies matching formula and cask metadata before mutation. Ordinary outdated casks use the normal exact-token lane. Checksummed self-updating and safely verifiable latest-version casks enter separate greedy-aware lanes only when the matching outdated query selects them; bare global --greedy remains prohibited. Current privileged casks stay quiet. A cask upgrade matches Homebrew's own default of quitting and reopening an app that declares a safe quit action; update-all --no-app-quit is the explicit one-run opt-out, deferring each observed running cask individually and passing Homebrew's --no-quit guard at mutation time. Mac App Store inventory uses the reviewed mas 7.x JSON-object stream from list --json and outdated --json --inaccurate; names containing parentheses remain ordinary data, and inventory does not start or cancel downloads. Before each mutation, interactive stdin and stderr are required and sudo -v visibly establishes macOS authorization in the foreground terminal process group while a separate watchdog enforces its deadline; the exact sudo -n mas update ID then runs in one owned process group under a conservative 30-minute backstop. A timeout names the exact app on elapsed notices, closes only its tree, and lets the next exact ID continue. Redirected, CI, or cron-style runs never prompt or mutate App Store apps; they report each one action-required and continue independent safe lanes. Pins, explicitly policy-declared vendor self-updaters, deprecated or disabled tools, manual/MDM ambiguity, and other owner-review items stay visible without becoming false failures or suppressing unrelated safe updates. Actual inventory, classification, mutation, or post-update verification failures remain nonzero while independent safe lanes continue. A nonzero Homebrew command always names its exact sub-lane and tokens, even when Homebrew emits no literal error line. Package maintenance and bounded Docker scratch cleanup retain their narrower ownership contracts, and arbitrary repository dependencies remain untouched. The optional action receipt shares the maintenance invocation ID and binds its immutable maintenance receipt by filename and SHA-256.
If ChatGPT exists in /Applications but Homebrew does not own it, update-all omits the conflicting Brewfile install and completes every safe lane. After the update transaction releases its lock, an interactive run first executes groundwork-app-adopt --check chatgpt, then asks Let Homebrew manage ChatGPT.app? [y/N] only when the current observation is eligible. A literal yes invokes the separately locked groundwork-app-adopt --yes chatgpt helper, which revalidates before mutation. A redirected run prints the review command and never prompts. Adoption uses Homebrew's identical-artifact path only after codesign --verify --deep --strict succeeds and bundle identity, signing team, ownership, running state, and MDM state are exact; those checks repeat after consent. It never forces, uninstalls, deletes, reinstalls, zaps, or takes over a device-managed app.
Fresh Groundwork setup declares only the current unified ChatGPT.app. ChatGPT Classic.app is the previous supported app and remains useful for known compatibility or existing Enterprise workflows, but it is a legacy duplicate for most people. Only after current-app ownership is verified may an interactive run separately ask Remove ChatGPT Classic.app while preserving shared data? [y/N]. Enter/no keeps it; yes invokes the separately locked groundwork-app-remove chatgpt-classic. A noninteractive run prints the command and never removes anything. The deprecated Codex.app / codex-app receipt is a third identity and routes only to groundwork-app-remove codex-app; a shared bundle identifier never establishes ownership. The removal command strictly verifies the current and legacy app signatures before reading their OpenAI signing teams, verifies exact bundle identities and Homebrew artifacts again after consent, refuses running or device-managed state, and shows what remains. Manual Classic moves to Trash; Homebrew-owned targets use only their reviewed cask uninstall. It never uses --zap or removes shared Application Support, preferences, chats, credentials, or unrelated data.
On a desktop Mac, a successful update ends with a Karabiner health postflight. An unusually large Core Service footprint or a Karabiner version change prints the observed PID, uptime, RSS, footprint, and compressed memory. A terminal gets Restart Karabiner now? [y/N]; yes invokes the supported restart helper and requires a changed root PID, while Enter or no preserves the service and prints the manual command. A headless or redirected run never restarts it and prints groundwork-karabiner-restart instead. Plain chezmoi update, init, and apply remain configuration operations and do not restart an already-running keyboard service.
A maintenance failure preserves completed updates, records the failed manager, helper, or Docker scratch lane, exits nonzero, and keeps the shell reminder active until a later repair; pending pnpm repair overrides ordinary cadence. Run update-all again for the normal retry. To retry package-manager maintenance without repeating upgrades, run groundwork-cleanup --yes; it keeps the normal pnpm cadence. Retry only Docker scratch maintenance with groundwork-docker-tidy --yes; a successful direct retry clears its own pending record. If Docker was intentionally removed and the retry is impossible, groundwork-docker-tidy --yes --clear-pending acknowledges only that reminder and preserves an audit note. Use groundwork-cleanup --yes --force only for the exceptional case where you deliberately want to ignore pnpm cadence. Repository dependency installations such as node_modules are not modified. Homebrew autoremove may remove installed orphan formula dependencies, while requested formulae and casks remain. The advanced groundwork-cleanup command reports exact package-manager candidates without changing persistent state. If a package manager was intentionally removed, groundwork-cleanup --yes --clear-pending <stage> acknowledges only its unrepairable reminder and preserves an audit note. update-all --skip-maintenance exists for an exceptional fast refresh, not the normal workflow.
Existing users receive one migration notice because an old update-all() function already loaded in a terminal can shadow the new executable. Run exec zsh once in those terminals. After that transition, future command updates take effect during update-all itself without another shell reload.
AI tools are the fast-moving exception: Claude Code stays on its vendor-supported latest path, while Codex and OpenCode refresh through Homebrew in the visible update-all path.
To see the other helper commands this setup installs, run groundwork-help or open the Command catalog.
To see what a change would do before committing to it, ask first:
$ chezmoi diff
# shows exactly what apply will change, and nothing else
Going deeper
- chezmoi — Quick start — the official fastest path from zero to a working Groundwork repo.
- chezmoi — User guide — the daily-workflow reference (add, edit, diff, apply, templates).
- GitHub does dotfiles — why dotfiles matter, with tools and examples.
- awesome-dotfiles — a big curated list of other people's setups to learn from.