The macOS OS-native layer β Homebrew, AeroSpace tiling, and desktop tooling over the shared Core.
Explore the docs Β»
View Demo
Β·
Report Bug
Β·
Request Feature
Table of Contents
dotfiles-MacBook is the OS-native layer for macOS (Apple Silicon / Intel) β
one node in a cross-platform dotfiles system. The shared Core (zsh, tmux,
Neovim, git, starship, mise) is authored once in
dotfiles-core and vendored under
core/ via git subtree, so a clone is self-contained. This repo adds only what
is specific to macOS: the Brewfile (brew bundle), Ghostty, the 1Password SSH
agent, the native pbcopy clipboard β plus a committed tiling-desktop layer:
AeroSpace, SketchyBar, and Karabiner, themed to match Core.
macOS is its own lineage β built directly on Homebrew, not stamped from the Fedora template. The full docs live on the documentation site.
The system is three layers, each building on the one below:
| Layer | Lives in | Owns |
|---|---|---|
| Core | dotfiles-core β vendored into every OS repo's core/ |
zsh, tmux, nvim, git, starship β identical everywhere |
| OS-native | dotfiles-{MacBook,Windows,Fedora,Arch,Debian,openSUSE,Alpine,Gentoo} (this repo among them) |
package manager, clipboard, paths |
| Role | dotfiles-Offense, dotfiles-Defense |
offensive / defensive tooling |
The Brewfile is a Ruby DSL (brew bundle); everything else is shell + config over Core.
A Mac and the Xcode Command Line Tools
(xcode-select --install) β that brings Git. bootstrap.sh provisions the rest
via Homebrew. Already have a Mac configured a different way? See the
migration guide to move onto this layout safely.
git clone https://github.com/dotgibson/dotfiles-MacBook ~/dotfiles-MacBook
cd ~/dotfiles-MacBook
./bootstrap.sh --links-only --dry-run # preview the symlink plan (changes nothing)
./bootstrap.sh # Homebrew + brew bundle + symlinks
exec zsh
./bootstrap.sh --macos-defaults # optional: apply system prefscore/ is a vendored copy and is already present in a clone β there is no
submodule step.
| Flag | What it does |
|---|---|
--dry-run, -n |
print every planned action; change nothing |
--links-only |
just (re)create symlinks, no installs |
--no-brew |
symlinks + mise, skip Homebrew and brew bundle |
--set-shell |
make the Homebrew zsh your login shell (chsh) |
--macos-defaults |
also run macos/defaults.sh (system prefs) |
--only zsh,nvim |
link ONLY these module groups |
--skip desktop |
link everything EXCEPT these module groups |
--uninstall |
remove the symlinks and restore backed-up files |
--quiet, -q |
show only CHANGES + the summary |
--json |
machine-readable summary on stdout (for automation) |
-h, --help |
the same list, from the installer itself |
Module groups are zsh nvim tmux git prompt tools desktop β the first six come
from Core, desktop is this layer's own (ghostty, fastfetch, aerospace,
sketchybar, karabiner).
| Code | Meaning |
|---|---|
0 |
clean run |
1 |
could not run (not macOS, missing core/, no Command Line Tools) |
2 |
usage error (unknown flag, bad --only/--skip selector) |
3 |
ran, but one or more steps failed β the box is degraded |
130 |
interrupted (Ctrl-C); bootstrap is idempotent, just re-run |
Exit 3 is the one to watch in automation. Some steps must not abort the run β
mise install, macos/defaults.sh, chsh, the tpm clone β so a failure there
leaves you installed but degraded (no runtimes, no tmux plugins). The closing
line says so, the failed steps are listed under the summary, and the exit code
distinguishes it from both a clean run and a bootstrap that never started.
A clean exit still is not a finished machine, so the run ends with a next steps
block β probed, not hardcoded, so it names only what is actually outstanding
and says nothing on a box where everything is done:
==> next steps
β’ Karabiner-Elements β open it once, approve its driver extension, then grant
it Input Monitoring (System Settings β Privacy & Security)
β’ git identity is still the seeded placeholder β edit ~/.config/git/local.gitconfig
β AeroSpace is running and responding
β tmux plugins installed (6)
β pre-commit hook installed
Two of those bootstrap now simply does: it installs the tmux plugins headlessly
(rather than leaving you to press prefix + I) and runs pre-commit install, so the
local lint and gitleaks gate exists from the first run. The β lines are the proof
they landed.
The GUI permissions are the ones only you can grant. macOS keeps those grants in a
system database that is unreadable without Full Disk Access, so bootstrap cannot ask
"was this allowed?" β each β there reports a behavioural check (the driver
extension is approved, AeroSpace answers its socket) and is worded as exactly that,
never as more.
--dry-run skips the block entirely: nothing ran, so every item would read as
outstanding.
One object on stdout, nothing else (the human log goes to stderr):
{
"dry_run": false,
"ok": false,
"linked": 40, "backed_up": 0, "seeded": 2, "skipped": 0,
"removed": 0, "restored": 0,
"tools": { "zsh": true, "starship": true, "mise": true, "fzf": true,
"nvim": true, "tmux": true, "git": true },
"errors": ["mise install failed β β¦; re-run: mise install"],
"warnings": ["not yet on PATH: starship β open a new shell, β¦"],
"next_steps": ["AeroSpace is not responding β launch it and grant it Accessibility β¦"]
}ok is true iff errors is empty, and mirrors exit 0 vs exit 3 β check it
alone if you want one field. warnings are notices that do not degrade the
run (a tool that just needs a fresh shell), so they never clear ok. tools
reports whether each headline binary resolved on PATH at the end of the run.
next_steps is the outstanding half of the closing checklist β work that is left
for a person, not a defect. It never clears ok either, because nothing failed;
without it a fleet-provisioning script reading ok alone would call a box finished
while its keyboard remapper is still inert. Empty on a machine with nothing left
to do, and empty under --dry-run, which probes nothing.
--uninstall --json emits the same object, and is the only mode that fills
removed / restored. It reports "tools": {} β an uninstall never probes
PATH, so an empty object there means "not measured", not "nothing installed".
If something goes wrong, --uninstall is the way back. It removes only
symlinks that point into this repo β never a real file, never a foreign link β
and restores the most recent .pre-dotfiles.* backup it made. Pair it with
--dry-run to see exactly what it would do first.
Only what changes with the OS. The heavy lifting β the shell modules, editor, and prompt β comes from vendored Core; this repo owns the macOS specifics:
Brewfileβ Homebrew packages (CLI + casks + fonts), the source of truthos/macos.zsh,os/macos.gitconfig,os/macos.confβ the macOS overlaysmacos/defaults.shβ thedefaults writesystem-preferences script (opt-in)aerospace/,sketchybar/,borders/,karabiner/,ghostty/,fastfetch/β the desktop + terminal toolingcore/β vendored fromdotfiles-core(read-only here; edit upstream)
The things worth knowing β Homebrew on /opt/homebrew vs /usr/local, the native
pbcopy/pbpaste clipboard, the 1Password SSH agent, and the macOS keychain β
are written up on the hub, alongside the migration guide for
moving an existing Mac onto this layout:
This is an OS-native layer, so the contribution rule is a boundary rule:
- Never hand-edit
core/. It is a vendored copy ofdotfiles-coreand is overwritten on the next sync. Fix shared config upstream indotfiles-core, runmake auditthere, thenmake syncfans it out here. - Keep changes genuinely macOS. If it would be identical on every machine, it belongs in Core; if it changes with the operator, it belongs in a role repo.
- Green the gate.
make lintbundles the repo-owned checks locally andpre-commit installmirrors them at commit time, so "passes locally" means "passes in CI". The full suite:
| Gate | Covers |
|---|---|
shellcheck Β· shfmt Β· bash -n |
repo-owned bash |
zsh -n |
the zsh entry files (no .sh extension, so the bash globs miss them) |
make test-repo |
bootstrap (incl. provision), the zsh loader, defaults.sh β ~150 assertions |
| file hygiene (CI) | .pre-commit-config.yaml's upstream hooks over the whole tree β whitespace, EOF newlines, line endings, merge markers, shebang β exec bit, yaml/json/toml |
make config-check |
every repo-owned .json / .jsonc / .toml parses |
make markdownlint |
repo-owned markdown, against .markdownlint.jsonc |
make secrets |
gitleaks over the repo-owned tree |
make verify-core |
the vendored Core subtree hasn't drifted β diffed byte-for-byte against the vendored subset of upstream at the recorded commit (CI sets VERIFY_CORE_STRICT=1 so "couldn't reach upstream" fails instead of skipping) |
actionlint |
the workflows themselves |
| macOS smoke (CI) | the entry points on real Darwin β clipboard, Brewfile, bootstrap |
CI requires a single aggregated ci ok context rather than each job by name,
so adding or renaming a leg needs no ruleset change.
Bugs and ideas: open an issue.
Distributed under the MIT License. See LICENSE for more information.
Garrett Allen - @gerrrrt - garrettallen2@gmail.com - LinkedIn
Project Link: dotgibson