Ansible playbook that provisions a fresh macOS machine into my working environment: window manager, terminals, editors, language toolchains, and a handful of custom scripts.
CLI frames in docs/cli/ are generated headlessly via freeze:
brew install charmbracelet/tap/freeze
bin/screenshots cli # regenerate CLI frames only
bin/screenshots desktop # requires a logged-in GUI session
bin/screenshots # bothClone into ~/system (the path is assumed by bin/bootstrap and some roles):
git clone git@github.com:HoganMcDonald/system_settings.git ~/system
cd ~/system
bin/bootstrapOn first run, bin/bootstrap:
- Installs Homebrew (if missing) and Ansible.
- Prompts you to populate
~/.vault_pass.txtwith the Ansible vault password. - Runs the full playbook against
localhost.
Run everything:
bin/bootstrapRun a single role by tag:
bin/bootstrap neovim
bin/bootstrap limaRoles are tagged one-to-one with their role name — see dotfiles.yml for the full list.
Tools
homebrew, git, asdf, devbox, direnv, lima, ollama, cli, zsh, tmux, pgcli, mycli, agents, nanobot, neovim, helix, zellij
Apps (Homebrew casks)
apps (Linear, Figma, Brain.fm), aerospace, browsers, kitty, ghostty, sol
Languages
lua, ruby, rust, javascript
Desktop & System
sketchybar (modular Lua status bar), karabiner, macos (system preferences)
The linux CLI (shipped by the zsh role) drops you into an Ubuntu VM backed by Lima. On first invocation it creates the VM with a writable home mount; every invocation installs an xdg-open shim inside the guest that forwards open calls back to the host via a queue file. A launchd agent on the host (com.hoganmcdonald.lima-open) tails the queue and runs open(1) on each URL.
linux # interactive shell
linux uname -a # one-off command
# inside the VM:
xdg-open https://example.com # opens in your host browserModular configuration built on SbarLua. Components live in roles/sketchybar/files/bar/components/ and use a fluent-API wrapper (Bar, Item, Bracket, Event, Animation) in roles/sketchybar/files/lib/.
Installs global config, rules, skills, and subagents for Claude Code, plus related agent tooling.
bin/dotfiles is a thin wrapper around git + Ansible that tracks which roles have been applied. State lives in ~/.dotfiles/lock.json (per-role git hash of the last successful apply, plus a timestamp).
dotfiles status # which roles have drifted since last apply
dotfiles apply # pull, run ansible only for changed roles, update lockfile
dotfiles apply git # force-apply a single role by tag
dotfiles sync # git pull + git push (no ansible)
dotfiles lock # manually snapshot current role hashesTypical loop: edit a role, commit, dotfiles sync to push, then on any machine dotfiles apply to pull and run only what actually changed.
Built around git-town stacked branches, git worktrees, and tmux. Worktrees live under <repo>/.worktrees/<adjective-animal>/.
hack feat/add-auth # git-town append → worktree → tmux session (nvim/claude/zsh)
hacks # list active hack worktrees and their session state
rehack # recreate tmux sessions for orphaned worktrees (post-reboot)
unhack feat/add-auth # tear down session + worktree, delegate branch cleanup to `git merged`The tmux session is named <parent>/<branch> so stacks nest cleanly inside an outer session.
Some apps can't run from two working directories at once (ports, singletons, local databases). swap temporarily moves a feature branch out of its worktree and into the main checkout; unswap reverses it.
swap feat/add-auth # main checkout → feature branch; worktree detached
unswap # restore main branch to main dir, reattach worktree
unswap --force # skip branch verificationUncommitted changes flow in both directions, so it's safe to keep editing during a swap.
For reviewing someone else's branch without disturbing your own stack. Checks out the branch in a linked worktree and opens a tmux session with Claude running the /review skill.
review feat/their-branch # worktree + tmux session at review/<branch>
unreview feat/their-branch # tear down session + worktree (never deletes the branch)Drop into an Ubuntu VM that shares your home directory. First run creates the VM with a writable home mount; every run installs an xdg-open shim in the guest that forwards browser/URL opens to the host via a queue file watched by a launchd agent.
linux # interactive shell
linux uname -a # one-off command
# inside the VM:
xdg-open https://example.com # opens in your host browserEnv vars: LINUX_VM_NAME (default default), LINUX_VM_TEMPLATE (default template://ubuntu).
Stash a connection URL under a short alias, then connect by name.
pgconnect "postgres://user:pw@host:5432/db" dev
pgcli "service=dev"
myconnect "mysql://user:pw@host:3306/db" dev
mycli -d devpgconnect writes ~/.pgpass (chmod 600) and ~/.pg_service.conf. myconnect updates ~/.myclirc with DSN aliases.
bin/
bootstrap # entry point
lock-role # records completed roles
dotfiles.yml # main playbook
hosts # ansible inventory (localhost)
roles/<name>/
tasks/main.yml # what the role does
files/ # static assets
templates/ # jinja2-rendered configs
defaults/main.yml
meta/main.yml # role dependencies
vault/ # encrypted variables (needs ~/.vault_pass.txt)
- macOS (Apple Silicon or Intel)
- Sudo access (prompted via
--ask-become-passwhere needed) ~/.vault_pass.txtfor the Ansible vaultterminal-notifier(optional) — used to notify on bootstrap completion



