Reproducible Mac agentic dev environment: Nix, Neovim, WezTerm, Herdr
- YT :: https://www.youtube.com/watch?v=5N-okeDdIuI
- Original title :: L8 Principal's Agentic Dev Environment From Scratch
Walkthrough of building a fully reproducible macOS development environment from a freshly installed machine, using Nix (via Determinate Systems' installer), nix-darwin, home-manager, and nix-homebrew to declaratively manage OS settings, packages, and dotfiles. Covers zsh, Starship, WezTerm, a from-scratch Neovim config (Lazy, Snacks, oil.nvim, Neogit/gitsigns, which-key), the Herdr terminal multiplexer for running multiple AI agent sessions, and a shared global AGENTS.md memory file distributed to all agent harnesses via home-manager symlinks.
Reproducibility via Nix
The core goal: apply the same config to any Mac, and recover instantly if an AI agent damages the system. Solution is Nix, a declarative/reproducible config system originally for NixOS (Linux) but made portable to macOS via the Determinate Systems installer (run their one-line install command, then the shell-refresh command it prints). A "dotfiles" repo is created on GitHub and git-initialized locally, with a symlink placed at a fixed, stable path so that all later scripts and configs can reference the dotfiles location consistently regardless of where the actual clone lives.
nix-darwin for macOS system settings
nix-darwin manages macOS-level settings declaratively. Setup:
- Copy the nix-darwin boilerplate into
flake.nix, renaming the default "system" label (from the example username) to something project-specific (here, "Mac"). - Pin nixpkgs/nix-darwin to a specific stable release rather than tracking unstable, to avoid surprises.
- Create
configuration.nixreferenced by the flake, with: =nix.enable = false= (since Determinate already manages the Nix install itself), =nixpkgs.config.allowUnfree = true=,nixpkgs.hostPlatformset per CPU architecture (Apple Silicon vs Intel),system.primaryUserset to the username, andsystem.stateVersion(set once, e.g. 6, and never touched again). - Activate with
sudo darwin-rebuild switch --flake .#Mac(first run installs missing deps and is slow). A helper scriptrebuild.shwraps this command so future changes are a single command to apply. - Practical settings applied through
configuration.nix: dark theme, fast key repeat with short delay, autohide menu bar, always show file extensions, autohide the Dock, Finder list view, clean desktop (no icons), and tap-to-click on the trackpad. All are declarative — no manual clicking through System Settings.
Homebrew via nix-homebrew
Installing Homebrew directly via its normal install script isn't reproducible across machines, so it's installed as a Nix package instead, using the nix-homebrew module. Setup:
- Add
nix-homebrewas a flake input and as a module inflake.nix. - In
configuration.nix, add a homebrew block withcasks(e.g. WezTerm) and set =cleanup = "zap"=, which removes any Homebrew package not declared in the Nix config on every rebuild — this forces all installs to go through Nix config instead of ad hocbrew install, keeping the whole system reproducible. - Rebuild, verify with
brew --version, then switch the default terminal over to WezTerm once it's installed.
home-manager for user-level config
home-manager manages everything under the home directory (as opposed to nix-darwin's OS-level scope). Setup:
- Add home-manager as a flake input/module, and reference it via a line binding the username to a
home.nixfile. - In
configuration.nix, ensure the user's home directory is set correctly (required for home-manager to work). home.nixcreates symlinks from real config locations (e.g.~/.config/wezterm) into paths inside the dotfiles repo. This means the dotfiles repo becomes the source of truth and stays version-controlled even when an app rewrites its own config at runtime, since the config directory itself is a symlink into the repo.- home-manager also installs user-level packages, including fonts (e.g. Hack Nerd Font, installed declaratively rather than downloaded manually), and sets environment variables (e.g. default editor = Neovim).
Shell: zsh, Starship
- home-manager config enables zsh autosuggestions (ghost-text completion from command history) and syntax highlighting.
- Custom
initContentis injected into zsh's rc: a keybind (Ctrl-F) to accept the autosuggestion ghost text, plus a set of shell aliases for frequently typed commands. - Starship is installed via home-manager for a customizable, better-looking shell prompt (kept simple here, but highly configurable).
WezTerm configuration
WezTerm is a fast, Rust-based, cross-platform terminal emulator (works consistently even on Windows), configured via Lua and hot-reloading on save. Config file: ~/.config/wezterm/wezterm.lua, symlinked from the dotfiles repo. Settings applied incrementally, each visible immediately on save: colorscheme "Rosé Pine Moon", font set to Hack Nerd Font at size 15, background opacity/blur adjusted, tab bar hidden when only one tab is open, and the window frame/decorations removed entirely for a clean, content-only window.
Neovim configuration from scratch
Config lives at ~/.config/nvim/init.lua (symlinked from dotfiles), loaded via home-manager after rebuild.
init.luajust requires other Lua modules (files under alua/subdirectory), keeping config modular.lua/config.lua(or similarly named) sets core Vim options: leader key = space; expand tabs to 2-space indents; absolute + relative line numbers (relative numbers let you jump n lines with e.g.5kinstead of counting manually); ignorecase + smartcase search; =clipboard=unnamedplus= to share the system clipboard; =scrolloff=16= to keep context above/below the cursor; persistent undo history across sessions.- Plugin manager: Lazy (by folke), the most widely adopted Neovim plugin manager. Boilerplate setup script loads every file under a
plugins/directory automatically. - Navigation plugins (
plugins/navigation.lua): - Snacks.nvim (folke) — picker, notifier, input modules enabled; keybinds: leader-F file picker, leader-S grep/search picker, leader-B buffer picker, gd go-to-definition.
- oil.nvim — file explorer that represents the filesystem as an editable buffer; leader-E opens it; shows hidden files; editing/saving the buffer performs the corresponding file operations (rename, copy via yank+paste, delete via
dd+ write). - Git plugins (
plugins/git.lua): Neogit (diff review/staging UI) and gitsigns (inline blame for current line). Neogit lazy-loaded on theBufWinEnterevent for faster startup. - which-key (folke) shown under a UI module — displays a popup of available keybinds after pressing the leader key, useful for newly added, not-yet-memorized bindings.
- Custom keybinds (
plugins/keys.luaor similar): Esc also saves the file (since Esc-to-normal-mode is usually followed by a save anyway); Ctrl-A selects all; and a fix for Vim's default paste-register behavior so that repeated pastes don't get overwritten by the deleted/replaced text (keeps the yanked text stable across multiple pastes). - LSP and Treesitter setup intentionally left out of scope (noted as well covered elsewhere).
Herdr: agent-aware terminal multiplexer
For running multiple AI agent sessions in parallel (kicking off a new agent while another works, reviewing output, etc.), a plain terminal isn't enough — need session/window/pane management. tmux is the classic option (used for years), and WezTerm has built-in multiplexing too, but the video switches to Herdr (https://terminaltrove.com/herdr/), a newer tmux-like, agent-aware multiplexer:
- Understands what coding agents are and integrates with most mainstream agent harnesses.
- Possibly the only terminal multiplexer that also works on Windows.
- Installed as a Homebrew formula via the nix-homebrew
brewlist (notcasks) inconfiguration.nix. - Config at
config.toml, symlinked from dotfiles; used mainly to set tmux-like keybinds to preserve muscle memory (defaults are fine if new to tmux). - Provides a side panel listing workspaces/agents plus a main terminal area; supports creating/naming tabs and splitting into panes like tmux.
- Key feature: real-time status integration — when running Claude Code inside Herdr, the side panel shows what the agent is doing. Combined with Claude Code's
/statuslinecommand (asked to show model name and % of context window used), Herdr surfaces that status info directly in its panel, consistently across different agent harnesses (not just Claude Code).
Global AGENTS.md memory file
To make all agent harnesses (Claude, Codex, OpenCode, "pi", Grok, etc.) behave consistently, a single shared memory file is authored once in the dotfiles repo (home/agents.md) and symlinked via home-manager into each harness's expected global-memory location (e.g. Claude's CLAUDE.md). Rules included:
- Never use em dash; use a plain dash instead.
- Never auto-add the agent as co-author in commit messages — the human remains accountable for code quality, so crediting the agent as co-author isn't seen as useful.
- Never manually modify CHANGELOG.md or other autogenerated files.
- When making technical decisions, don't over-weight development cost; prefer quality, simplicity, robustness, scalability, and long-term maintainability. Rationale: LLMs are trained on human data, and humans estimate dev cost in days/weeks/months even though agents work much faster — this biases agents toward cheap, unscalable solutions unless explicitly corrected.
- When fixing bugs, always first reproduce the bug end-to-end as close to the real user experience as possible, to avoid the agent "fixing" a problem that doesn't actually exist.
- When doing end-to-end testing, be picky about UI correctness and pixel-perfection — fix clearly-off visual issues encountered along the way even if unrelated to the current task.
- Hold engineering quality to a high bar generally: fix lint/test failures and flaky tests encountered along the way, even if not caused by the current work.
Fully reproducible result
After the rebuild script incorporates all of the above, the entire environment — OS settings, Homebrew packages, user packages, shell, WezTerm, Neovim, Herdr, and the shared agent memory file — can be reproduced from scratch on any freshly installed Mac by cloning the dotfiles repo and running the rebuild script. The finished dotfiles repo is published on the author's GitHub (linked from the video description, https://github.com/kunchenguid/dotfiles).