Personal macOS configuration for Fish, Neovim, tmux, git and a few dozen CLI
tools. Files live in config/ and are symlinked into ~/.config/ by
dotbot.
Three machines run this: macbook-m5-pro, mac-mini and macbook-2019 — two
Apple Silicon and one Intel, which is why so much here is careful about the
Homebrew prefix and the architecture.
just # every recipe, from any directory
just doctor # what has rotted on this machine — read-only
just update # the routine update, including all backups
just lint # what must pass before committingFull reference: docs/commands.md. If just doctor reports
something and you want to know what it means,
docs/troubleshooting.md has one section per finding.
Five things have to be done by hand before setup.sh can run, because each one
is either a login, a licence, or a chicken-and-egg problem. setup.sh's
preflight step checks all of them and refuses to continue if one is missing,
so this list is verified rather than merely documented — you will be told what
is wrong before an hour of brew bundle, not during it.
1. Xcode command-line tools, which supply git, curl and tic:
xcode-select --install
sudo softwareupdate --install-rosetta # Apple Silicon; some casks need it
sudo xcodebuild -license accept2. Homebrew, which supplies dotbot, fish and everything the packages step
installs:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"3. A password manager and an SSH agent, because the private submodule is cloned over SSH:
brew install --cask proton-pass
brew install --cask secretiveLog into the password manager, then start Secretive, create a key and add the public half to GitHub. Keys live in the Secure Enclave, not on disk, so the agent socket has to be exported before the first clone:
export SSH_AUTH_SOCK="$HOME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh"4. The hostname, which decides which Brewfile is used. A fresh Mac reports
something like Mac.local, and macOS changes the Bonjour name with the network,
so set it permanently:
sudo scutil --set HostName <hostname>It must match a file in homebrew/. A hostname with no Brewfile is a hard
failure that names the ones that exist; DOTFILES_HOST=<name> overrides it for
a single run.
5. The clone, at ~/.dotfiles — install.conf.yaml links ~/.agents and
friends relative to it:
git clone git@github.com:kogakure/dotfiles.git ~/.dotfilesThen sign into the App Store, so the mas entries in the Brewfile can install,
and run the thing:
cd ~/.dotfiles
./setup.shIt is a step registry, not a linear script. Each action is a step_<name>
function and every one runs through run_step, so a single failure is reported
by name in a summary at the end instead of aborting the run — and completed
steps are recorded, so a re-run resumes rather than redoing an hour of
brew bundle.
./setup.sh # every step, in order
./setup.sh --list # the ordered step list
./setup.sh --dry-run # print every mutation, change nothing
./setup.sh --only link,gnupg # just these
./setup.sh --skip macos,services # everything but these
./setup.sh --from editors # that step and everything after it
./setup.sh --force # ignore the resume state file
./setup.sh --no-interactive # skip the steps that prompt--dry-run is inert, not nominal: it changes nothing, never asks for a
password, and writes no state. CI asserts all three on every commit.
The ordering constraints, the resume-state file and which steps are safe to
--only are in docs/commands.md.
Afterwards:
just doctor # confirm the machine matches what the repo describesgit pull && git -C private pull # scripts first, and other machines' backups
just update # everything below, then all backupsjust update asks for the admin password once, then updates Homebrew, mise
tools, the Rust toolchain, Ruby gems, tmux, gh, herdr, fish and Neovim plugins,
runs the maintenance sweep, takes every backup (Claude, Codex, Grok, Homebrew,
preferences, launch agents) and finally applies macOS software updates. Each
step is isolated: one failure is named in the summary and the rest still run.
It writes to two places worth reviewing afterwards — homebrew/<hostname>,
which is re-dumped from what is installed, and the private/ submodule. Commit
private/ first, then the pointer in this repo.
The GPG key export is deliberately not part of it, because it writes a secret
key and can block on a pinentry prompt. Run just backup --gpg when you want
it; every summary names it as skipped so it cannot be forgotten.
- The shell config is generated. Environment,
PATH, aliases and tool hooks are described once inshell/*.specand emitted for bash, zsh, fish and nushell. Edit a spec, runjust generate, commit both. Never hand-edit anything undergenerated/,config/fish/conf.d/0*.fishorconfig/nushell/env.nu— each says so in its header andjust lint driftfails if the output is stale. - The Brewfiles are generated too.
homebrew/<hostname>isbrew bundle dumpoutput, rewritten on everybin/update. A line you add by hand survives until the next update on that machine; install the package instead. private/is required for full functionality and is not public. Without it there is no git identity, no application preferences and no agent config.just doctornever writes,just cleannever deletes without--apply. Both are held to that byjust lint unitand by CI.
| File | Covers |
|---|---|
| docs/commands.md | Every script and just recipe, in detail |
| docs/architecture.md | Symlinks, the private submodule, per-host configs, file layout |
| docs/environment.md | Fish, Neovim, tmux, mise and the version managers, git |
| docs/guardrails.md | just lint, the pre-commit hook, CI, and what each check exists to catch |
| docs/troubleshooting.md | Each just doctor finding and the command that fixes it |
| docs/git-workflow.md | Conventional commit format |