Configuration files for zsh, vim, git, and more. macOS only. The login
shell is zsh; install and helper scripts still run under bash 3.2 because that
is what macOS ships.
Installation requires bash, curl, and git.
git clone git@github.com:sidwood/dotfiles.git
cd dotfiles
./install.shThe install script presents an interactive menu where you can select which
components to install. It also points this repository's core.hooksPath at
.githooks so the commit-msg house style is enforced.
Select installations (↑/↓/k/j navigate, Space toggle, Enter confirm):
> [x] Install Homebrew packages and applications.
[x] Apply macOS system defaults.
[x] Symlink dotfile packages with GNU Stow.
[x] Set up mise with default runtimes.
[x] Set up MLX for Apple silicon.
[x] Install global pnpm packages.
[x] Install vim plugins.
Use arrow keys or j/k to navigate, space to toggle options, and enter to
confirm.
The dotfiles are organized as GNU Stow
packages. Each top-level directory is a package that gets symlinked to $HOME.
dotfiles/
├── agents/ # Global agent memory shared by every AI harness
├── bin/ # Custom executables on PATH via ~/.local/bin
├── ghostty/ # Ghostty terminal config
├── git/ # Git config and global ignore
├── herdr/ # Herdr workspace manager (Solarized Dark, tmux-first keys)
├── htop/ # htop process viewer config
├── iterm2/ # iTerm2 terminal config (macOS)
├── misc/ # Miscellaneous dotfiles (.editorconfig, .agignore, etc.)
├── mutt/ # Mutt email client config
├── npm/ # NPM configuration
├── nvim/ # Neovim config (Lua with lazy.nvim)
├── opencode/ # Local and remote coding-agent providers and roles
├── profile/ # Shared environment variables (.profile)
├── ruby/ # Ruby/Bundler/IRB config
├── shell/ # Unified shell config (aliases, functions, init)
├── ssh/ # SSH config (1Password agent)
├── tmux/ # Tmux config
├── vim/ # Vim config and plugins (vim-plug)
├── yazi/ # Yazi file manager config
└── zsh/ # Zsh config with zinit and powerlevel10k
The herdr/ package folds to ~/.config/herdr/config.toml. It uses Herdr's
built-in solarized theme with canonical Solarized Dark tokens so the chrome
matches Ghostty. The native sidebar split stays intact: Spaces above, Agents
below. In-app toasts appear bottom-right; Herdr colors them by kind (blue for
finished, red for needs-attention, yellow for config warnings). Rounded pane
borders are not configurable in Herdr 0.8.
Keys follow the tmux config: Ctrl-A prefix, h/j/k/l pane focus,
Ctrl-H/Ctrl-L tab cycling (keep Ctrl held: A then H/L), , to rename
the tab, |/\ and -/_ splits, and r to reload.
Herdr extras stay on unused chords: g for the workspace/agent picker, b or
m for the sidebar, d or q to detach, Shift-R for resize mode. prefix+% and prefix+" are extra
split aliases. prefix+a forwards a literal Ctrl-A into the focused pane;
double Ctrl-A is Herdr's built-in fallback. Tmux itself is unchanged.
Reload a running server after Stow with herdr server reload-config.
Selecting the Stow installation also runs Herdr's own OpenCode integration
installer when both commands are available. It generates
~/.config/opencode/plugins/herdr-agent-state.js, which lets OpenCode report
its lifecycle state and resumable session identity to Herdr. The generated
file is deliberately not stored here so it stays matched to the installed
Herdr version. Check it with herdr integration status; re-running
./install.sh with Stow selected refreshes it.
The shell/ package provides configuration sourced from zsh:
~/.config/shell/aliases- Common aliases~/.config/shell/functions- Utility functions~/.config/shell/init- Tool integrations (mise, zoxide, fzf)
On Apple silicon, the installer creates an isolated Python 3.13 environment at
~/.local/share/venvs/mlx and installs the pinned MLX and MLX LM versions from
bin/.local/share/mlx/requirements.txt. Re-run the setup after changing those
pins:
mlx-setupThe named coding models are pinned in bin/.local/share/mlx/profiles.tsv, with
the default selected by default-profile.txt. Download one profile or both and
start its local OpenAI-compatible endpoint with:
mlx-model-download qwen36
mlx-model-download all
mlx-server-start qwen36cm retains Qwen3-Coder-Next-4bit as the default interactive OpenCode
worker. cmq launches Qwen3.6-35B-A3B-OptiQ-4bit; the equivalent explicit
form is cm qwen36. Each command starts its own server when necessary, and the
two servers can coexist on the 128 GB M5 Max. Use opencode-mlx --profiles to
list their names and ports. Run frontier-review "<requirements>" for an
explicit review.
For an autonomous fleet job, use cm run "<task>" from a clean Git worktree.
The launcher runs the local worker, asks Codex GPT-5.6 Sol and Claude Opus to
review its working diff independently in read-only modes, then sends their
findings back to the worker for one remediation pass. Use mlx-server-stop to
release the model's unified memory.
Run cmab "<implementation task>" from a clean repository to compare both
models. It creates two disposable branch clones, gives each worker the same
task, and saves their logs, timings, Git status and complete patches under
~/.local/state/mlx/ab/<timestamp>/ without changing the source checkout or
spending frontier-model credits. Stop one named server with
mlx-server-stop qwen36, or stop both with mlx-server-stop all.
Use mlx_lm for direct model access and mlx-python for Python code that
imports MLX. Downloaded Hugging Face models remain in the normal user cache and
are not removed when the rebuildable Python environment is uninstalled.
oMLX is installed from its Homebrew tap as the always-on model server. Run the idempotent setup once after installing the Brewfile:
omlx-glm-setupThe command downloads mlx-community/GLM-4.7-Flash-4bit, binds oMLX only to
127.0.0.1:13308, generates a per-machine API key, and pins GLM so it is
preloaded whenever the login service starts. It also snapshots the previous
Hermes configuration before making glm-4.7-flash-4bit the primary model with
a 65,536-token context window. The API key is written only to local oMLX and
Hermes configuration; it is never stored in this repository.
The setup keeps oMLX's paged SSD and hot prefix caches enabled and uses a deterministic non-thinking tool-agent profile at temperature 0 and top-p 1.0. Thinking is forced off because the 4-bit MLX checkpoint can enter repetition loops after tool results when thinking is enabled. Speculative decoding remains disabled until it is validated against Hermes's large tool prompt.
Use omlx stop, omlx start, and omlx restart to control the managed
service. The model remains in the shared Hugging Face cache if oMLX is
uninstalled.
The Brewfile installs Flutter and CocoaPods, and Xcode from the Mac App Store. Building or simulating an iOS app also needs a one-time ritual that the Brewfile cannot express, because it takes root and a licence agreement. Run the idempotent setup once Xcode is installed:
flutter-ios-setupThe command points the active developer directory at full Xcode, accepts the
Xcode licence, downloads the iOS platform and simulator runtime, installs
Xcode's deferred first-launch components, precaches Flutter's iOS engine
artifacts, and finishes with flutter doctor. Each step detects work already
done and skips it, so re-running after an Xcode upgrade only fetches what is
missing. The three steps that take root announce themselves before prompting.
Note that mas install needs root in mas 7, so Xcode itself is
sudo mas install 497799835 rather than part of brew bundle.
cd /path/to/dotfiles
./uninstall.shThe uninstall script presents a similar interactive menu:
Select uninstallations (↑/↓/k/j navigate, Space toggle, Enter confirm):
> [x] Uninstall Homebrew packages and applications.
[x] Reset macOS system defaults.
[x] Remove dotfile package symlinks with GNU Stow.
[x] Uninstall vim plugins.
An alternative to git worktree, following Avdi Grimm's
You probably don't need git worktrees.
Worktrees bake absolute paths into config files, which breaks containers and
makes them awkward to move. A plain local clone has none of those problems, and
git hardlinks the object files when cloning locally — so a second copy of a repo
is near-instant and costs almost no disk.
The bin/ package wraps the bookkeeping in five commands:
gbca ~/code/myapp feature-x # clone ~/code/myapp -> ~/code/myapp.feature-x
gbc # list branch clones, with status and ahead/behind
gbc --pr # same, plus each branch's PR state from gh
gbcd # fzf-pick a branch clone and cd into it
gbcs # re-sync ignored local files from the source
gbcr ~/code/myapp.feature-x # remove one clone, with safety checks
gbcp --dry-run # show which clones have landed and can go
gbcp # remove the clones whose PRs merged or closedEach has a long form as a git subcommand — git bc-add, git bc-list,
git bc-sync-extras, git bc-rm, git bc-prune. Use -h for usage: git
intercepts --help on a subcommand and looks for a man page instead.
gbca clones from a local seed, repoints origin at the real remote so nothing
depends on the seed, and copies across the gitignored files a fresh clone needs
(.env and variants, .envrc, Rails credentials keys,
docker-compose.override.yml, Procfile.local, mise local config). Rebuildable
trees like node_modules are never copied.
The seed's own copy of a branch always wins, unpushed commits included. If the
seed has the branch but is parked elsewhere, gbca checks it out in the seed
before cloning and restores the previous branch afterwards; a seed with
uncommitted changes is refused rather than disturbed. This matters because a
local clone only materialises the source's current branch — the rest arrive as
remote-tracking refs, which the first fetch --prune deletes whenever the server
has never seen them. Branches that exist only on the server are tracked as
normal, and a name that exists nowhere starts a new branch off the seed's HEAD.
A clone is marked as a branch clone by a bc.source entry in its local git
config. Base clones lack it, so gbcr and gbcp will not touch them. gbcr
also refuses a clone with uncommitted work, one holding commits that exist
nowhere else, or the one the current shell is sitting in — --force overrides
the first three, nothing overrides the last.
"Nowhere else" counts the seed, not just the remote. A branch cloned before it
was pushed sits above every remote ref while risking nothing, since the seed
still holds every commit; refusing there would mean reaching for --force on
routine cleanup, which is how the one clone that mattered eventually gets
deleted. Commits absent from both the remote and the seed still block removal,
and a seed that has since been moved or deleted falls back to comparing against
the remote alone.
Three per-repo config keys, all set on the base clone:
bc.source- written bygbca; marks a directory as a branch clonebc.postadd- command run in a new clone once the branch is checked outbc.extras- extra file patterns forgbcsto copy (repeatable)
git -C ~/code/myapp config bc.postadd 'mise install && npm ci'
git -C ~/code/myapp config --add bc.extras 'config/local.yml'Two layers, both plain markdown so any agent harness can read them:
AGENTS.mdin this repo (symlinked asCLAUDE.md) — conventions for working on the dotfiles themselves. Only loads when an agent works in this repo.agents/.config/agents/AGENTS.md→~/.config/agents/AGENTS.md— global memory, loaded in every session in every repository. Machine-wide tooling likegit bc-*is documented here, since a project-scoped file cannot advertise something installed to$HOME.
Every harness reads a different filename in a different directory. Rather than
keeping a copy per tool, install.sh symlinks each one at the single canonical
file:
~/.config/agents/AGENTS.md # canonical, the only file to edit
├── ~/.claude/CLAUDE.md # Claude Code
├── ~/.codex/AGENTS.md # Codex
├── ~/.grok/AGENTS.md # Grok
├── ~/.config/opencode/AGENTS.md # OpenCode
├── ~/.cursor/rules/global-agent-memory.mdc # Cursor
├── ~/.gemini/GEMINI.md # Gemini CLI
└── ~/.pi/agent/AGENTS.md # Pi
Add a harness by adding one <command>|<path> line to the harnesses table in
link_agent_memory(). Links are only created for harnesses that are actually
installed, so $HOME does not collect config directories for tools you do not
have. Re-running is a no-op, and any pre-existing real file is moved to .bak
before being replaced.
Every path was verified against the tool itself rather than assumed — Codex from
strings in its binary, Grok from its shipped docs/user-guide, OpenCode from its
config root, Pi from its agentDir default, Cursor and Kimi from their docs and
issue tracker. Two are worth knowing about:
- Cursor does not read a home-level
AGENTS.md. Its global rules are.mdcfiles in~/.cursor/rules/, which is where the link points. Cursor loads a rule in every chat only when it carriesalwaysApply: truefrontmatter, and the canonical file deliberately has none — frontmatter would be dead weight in every other harness's context window. If Cursor does not pick the rule up automatically, enable it in Cursor's own rules settings. - Kimi is deliberately absent. It only reads
AGENTS.mdfrom the working directory; global support is an open, unimplemented request (kimi-cli#2152). Linking~/.kimi/AGENTS.mdtoday would create a file nothing reads;link_agent_memory()records the line to add when it ships.
Keep the canonical file short and free of meta-commentary. Every agent loads it into context on every session, so it should carry only what an agent needs to act on — not notes about the file itself, which belong here instead. Anything that should not enter git history does not belong in it at all.
OpenSSH is installed by Homebrew. The SSH daemon is not enabled automatically.
Prefer Apple’s built-in Remote Login (system sshd), not Homebrew’s openssh
daemon:
- GUI: System Settings → General → Sharing → Remote Login
- CLI:
sudo systemsetup -setremotelogin on
Check status with sudo systemsetup -getremotelogin or
sudo launchctl print system/com.openssh.sshd. Turn it off with
sudo systemsetup -setremotelogin off.
Some projects require secrets stored in 1Password (e.g., private GitHub Package
registries). The profile/.profile exports environment variables using
1Password secret references.
To inject secrets when running npm commands, use op run:
op run -- npm installThis replaces secret references (e.g., op://Personal/GitHub Registry Token/token)
with their actual values for the duration of that command.
The c alias launches opencode-framework. This wrapper uses op run to
resolve the machine-local ~/.config/opencode/framework.env reference before
starting OpenCode, so the Framework Desktop API key is never stored in the
repository. Interactive sessions disable op run output masking so terminal
applications retain direct TTY access; automated runs keep masking enabled.
This repo includes a template at shell/.config/shell/local.env.tpl for
machine-local API keys using 1Password secret references.
During ./install.sh, the script resolves that template with op run and
generates:
~/.config/shell/local.env
The generated file is sourced by shell/.config/shell/init, overwritten on
each install run, permissioned to 600, and ignored by git.