- macOS and Linux development environment
- One-command setup, safe to re-run
- Version-controlled and portable across machines
Setup runs in four steps, all driven by interactive menus (arrow keys to move, space to toggle, enter to confirm):
- Install software β shell, dev toolchain, and apps from
.brewfile - Apply personal configuration β symlinks, user-level defaults (always runs)
- Extras β optional perks in a checkbox menu; anything that needs sudo/admin says so in its label
- AI tools β optional checkbox menu (
claude-codeβ Claude Code + Codex config,agent-reach,claude-plugins); see AI setup
git clone https://github.com/cassmtnr/dotfiles.git ~/dotfiles
cd ~/dotfiles
# Review .brewfile, .aliases, .functions, and .defaults first.
./install.shWhile a step runs you get a spinner, its elapsed time once it passes three
seconds, and the latest line of its output, so a long brew bundle shows what
it's fetching instead of looking hung. Each finished step collapses to one
status line β β when it did the work, β when it had nothing to do (wrong
platform, already installed, prerequisite missing) or you skipped it, β when it
failed β and the run ends with a summary table. A β line says why. A β prints
the exit code and the last 15 lines inline, and keeps the full output in a temp
file whose path it shows. If a step fails you choose retry (fix the problem in another
terminal first if needed), skip, or abort β a flaky download doesn't
kill the run. Pass -v/--verbose to stream full command output instead of
the one-line-per-step view.
The whole UI is pure bash (macOS's stock bash 3.2, zero dependencies), so it works on a fresh machine, offline, with nothing installed.
Steps 3 and 4 have nothing pre-selected and are skipped without a terminal, so
a plain or non-interactive run (piped over SSH, CI) installs no extras and no
AI, never prompts, and records failures in the summary instead of stopping.
Use ssh -t host to get the menus remotely.
Idempotent β re-run anytime to add extras or AI tools; an aborted run resumes cleanly on the next run.
The script prints the detected OS at the start and tailors itself to it: GUI apps are Homebrew casks and install on macOS only (skipped on Linux β a VPS gets CLI tools only), and the extras menu shows only the perks relevant to the platform.
| Extra | Privileges |
|---|---|
brew-upgrade |
none β upgrades the .brewfile formulas (casks self-update) |
icons (macOS) |
writes to the app bundle β admin if it sits in /Applications |
macos-admin (macOS) |
needs admin β system-level defaults write |
motd (Linux) |
needs sudo β installs to /etc/update-motd.d |
Everything is user-level by default. The script asks one yes/no question
up front β "allow steps that need admin rights?" (apt-get, /etc changes,
the official Homebrew install) β defaulting to yes on Linux (servers, root
available) and no on macOS (managed work laptops just work).
Homebrew adapts to that answer. With admin it uses the official installer
(/opt/homebrew or /home/linuxbrew/.linuxbrew, precompiled "bottles", fast).
Without admin it untars Homebrew into ~/.homebrew β no sudo needed β but
Homebrew stamps most bottles with the default prefix, so anything not tagged
:any/:any_skip_relocation builds from source there (gnupg, openssh,
openssl, python, git, zsh, coreutils β¦). Needs Xcode Command Line
Tools on macOS. Either way brew runs user-level after. If Homebrew already
exists, neither path runs.
Because that build cost is real, Step 1 only installs β it never upgrades
(brew bundle --no-upgrade). Otherwise every re-run would recompile whatever
moved upstream that week, turning a no-op into an hour. Upgrading is a
deliberate choice: pick brew-upgrade from the Step 3 menu. Before the step
runs, the script names any formula that is about to be compiled, so a long
silent build is expected rather than mistaken for a hang.
Sudo-needing extras are different: they never run unless you select them in the menu, and selecting one is the authorization β they prompt for sudo themselves and skip with a warning if it's unavailable.
./update.sh # Pull this repo, then .ai on main (--ff-only, skips dirty trees)
./install.sh # Re-run after pulling when symlinks or packages changedupdate.sh only pulls β symlink, extension, package, and defaults refresh all
live in install.sh (idempotent; re-select step 4 for AI tooling).
dotfiles/
βββ install.sh # Setup in four steps: install / configure / extras / ai-tools
βββ update.sh # Pull this repo + .ai on main (refresh via install.sh)
βββ .brewfile # Homebrew packages (casks skipped on Linux)
βββ lib/
β βββ common.sh # OS detection, logging, Homebrew PATH
β βββ ui.sh # Terminal UI: menus, confirms, step runner β see UI.md
β βββ install.sh # Step 1: software installation
β βββ configure.sh # Step 2: symlinks, VSCodium, user-level defaults, shell
β βββ extras.sh # Step 3: optional perks (sudo/admin needs labeled)
β βββ ai.sh # Step 4: AI tools (Claude Code, Codex, agent-reach) β opt-in
βββ .editorconfig # Cross-editor coding standards
βββ .zshrc # Shell configuration
βββ .zshrc.local.example # Template for machine-specific overrides
βββ .zshenv # Environment variables
βββ .functions # Utility functions (mkd, killport, weather)
βββ .aliases # Shell aliases (25+)
βββ .ssh-agent # SSH agent management
βββ .completion # Shell completions
βββ .starship # Starship prompt configuration
βββ .defaults # macOS system preferences
βββ capture-setting.sh # Capture a changed macOS setting into .defaults
βββ .ghostty/ # Ghostty terminal (Nord theme, custom keybindings)
βββ .ssh/config # SSH configuration template
βββ .ai/ # AI CLI config (separate private repo,
β # cloned here by install.sh step 4)
βββ config/ # mcporter config for AI search (β ~/.mcporter, via step 4)
βββ .1password/ # 1Password SSH agent config
βββ .vscodium/ # VSCodium settings, extensions, custom icon
βββ .lazydocker/ # LazyDocker terminal UI configuration
βββ .motd/ # Message of the Day scripts (Linux/VPS)
βββ index.html # GitHub Pages shell (renders this README)
βββ .github/workflows/ci.yml # CI: shellcheck + install on macOS/Ubuntu
βββ docs/superpowers/plans/ # Design docs
βββ CHANGELOG.md # Notable changes
βββ TODO.md # Deferred work
βββ log.md # Append-only project log
To make a System Settings change reproducible on future machines, capture it
into .defaults:
./capture-setting.sh # 1. run it β it snapshots current preferences
# 2. change ONE setting in System Settings, wait ~2s
# 3. press EnterThe script detects which preference keys changed, filters out macOS churn
(timestamps, counters, caches), and appends ready-made defaults write lines
to .defaults β review with git diff .defaults, optionally move the lines
into a themed section, and commit. Keys the change removed are printed for
information but not appended (macOS often deletes a key to mean "back to
default"). If more than 12 keys changed, nothing is appended β that's
background noise; re-run and change only one thing. Captured settings apply
to new machines via install.sh (some need logout/login to take effect).
lib/ui.sh is the installer's whole UI: the spinner, the β/β/β status lines,
the arrow-key menus, the retry/skip/abort recovery, and the summary table that
sets the exit code. It's one self-contained file in pure bash 3.2 with no
libraries, so you can copy it into any project that runs a list of steps and
wants to report them honestly.
UI.md is the full reference: quickstart, the step contract, every
function, configuration, and a worked example. Drop both files into a project
and point Claude at UI.md.
- SSH: Edit
~/dotfiles/.ssh/configwith your key paths and hosts - SSH Agent: Edit
.ssh-agentwith your key paths - Git:
git config --global user.name/user.email - Local overrides:
cp .zshrc.local.example .zshrc.localand edit for machine-specific settings (.zshrc.localis gitignored) - AI tooling (optional): pick it in step 4 of
./install.shβ see AI setup
AI tooling is step 4 of ./install.sh β an opt-in checkbox menu, with
nothing pre-selected and skipped without a terminal, so a plain or
non-interactive install sets up none of it. Re-run ./install.sh and select
what you want in the AI step:
claude-codeβ links AI config into~/.claude/and~/.codex/and installs Claude Code (plus Gemini on Linux)agent-reachβ internet access channels for AI CLIsclaude-pluginsβ Claude Code plugins (needs a logged-in CLI)
It's all user-level (no sudo) and idempotent β re-run to update or add more.
The configuration itself lives in a separate private repo,
cassmtnr/ai-config, cloned to .ai/ and gitignored here. Step 4 offers to
clone it and skips cleanly when it can't, so an install without access to it
still finishes. That repo's README covers the instructions file, skills,
Agent Reach channels and the plugin list.
Migrating a machine that predates the split. Pulling the split deletes the
tracked files under .ai/ but leaves everything that was gitignored, so .ai/
stays behind holding no repo and step 4 tells you to move it aside. Run
mv .ai ~/.ai.bak, then ./install.sh. Afterwards copy back what only lived
there: codex/config.toml, and any skills the machine installed into
common/skills/ as symlinks. The backup goes outside the repo because a
backup left in this directory is untracked, and git clean -fd deletes it.
Two ways this repo can empty the clone. Git does not know .ai/ holds
another repo, so ordinary operations here reach inside it.
- Any checkout that crosses the commit which untracked
.ai/deletes those 26 files from disk, because git still sees them as tracked files being removed. Arebasedoes it every time: it checks out the pre-split base first, then replays the split. So dogit checkout <pre-split-commit>,bisect, andreset --hardbackwards past it. git clean -xfddeletes the whole clone, because ignored files are what-xremoves.
Neither loses anything, since ai-config holds it all. Recover the first with
git -C ~/dotfiles/.ai restore ., the second by re-running ./install.sh.
Safety hooks β block-dangerous-commands.js blocks dangerous Bash commands via PreToolUse hooks at three levels (fails closed on malformed input):
- Critical: filesystem destruction, disk operations, fork bombs, git history rewriting
- High (default): git write ops, elevated privileges, secrets exposure, publishing/deployment, database ops
- Strict: cautionary patterns (
git checkout .,docker prune)
VSCodium (open-source VS Code without Microsoft telemetry). Settings, extensions, and a custom icon are managed in .vscodium/:
install.shhandles symlinks and extensions automatically- Custom icon is an extra (re-run
./install.sh, selecticons) β writing to /Applications bundles needs admin rights on managed Macs brew()wrapper in.functionsre-applies the icon after upgradesalias code="codium"for transparent compatibility
- Never commit actual SSH keys β only configuration templates
- Use
.zshrc.localfor private/sensitive configurations - SSH keys should have
600permissions .gitignoreprotects sensitive files
- Slow startup:
time zsh -lic exitand uncommentzmodload zsh/zprof/zprofin.zshrc - SSH issues:
ssh -T git@github.comorssh -vTfor debug - Homebrew:
brew doctorandbrew update - macOS defaults:
.defaultsis audited for macOS 26+ / Apple Silicon β dead settings are removed periodically
CC0 1.0 Universal