Thirteen steps from a factory-fresh Mac to a terminal, editor, and agent setup you will want to live in. Every step is a command you can read first, run yourself, and stop after.
Developers disagree about almost everything. But WezTerm, Neovim, NvChad, and Herdr together make the most painless and productive setup for an AI-native engineer since the advent of sliced bread. Here is what you will have by the last step.
terminal
WezTerm with the coolnight palette, a Nerd Font, and a little glass
shell
Stock zsh, Powerlevel10k, autosuggestions, and syntax highlighting
editor
Neovim with NvChad, plus VS Code, JetBrains, and Xcode when you need them
agents
Herdr running Claude Code and friends across every project at once
languages
One version manager per language, so node is never the wrong one
secrets
GPG and git-crypt, plus SSH keys that stay on the Mac that made them
Four rules for the walk
No bootstrap script. Each step is something you choose to run. Read it, paste it, check it, move on.
Pin major versions, allow minor drift. A project's own version file always wins.
One manager per language. Homebrew does not get to own your runtimes, with Go as the one exception.
Keys stay put. SSH keys are generated fresh on each Mac. Tokens live encrypted or in a password manager, nowhere else.
Everything assumes Apple Silicon, where Homebrew lives at /opt/homebrew. On an Intel Mac, swap that for /usr/local. Tick each step as you finish it. Your progress stays in this browser, and the tab bar keeps count.
Lay the foundation
Three steps that everything else stands on.
~/dream/01-macfoundation✓
1. Know your Mac, then lock it
Before you install anything, find out what you are working with. The architecture decides where Homebrew lives and which native binaries you get later.
uname-m# arm64 on Apple Silicon, x86_64 on Intelsw_vers# your macOS versionecho$SHELL# /bin/zsh on any modern Mac
arm64Apple Silicon/opt/homebrew
x86_64Intel/usr/local
One line of output picks the path everything else builds on.
Then, before real secrets ever touch this machine:
Install all updates from System Settings, General, Software Update.
Sign in to your Apple Account so the App Store, iCloud Keychain, and Find My work.
Turn on FileVault under Privacy & Security, and save the recovery key in your password manager.
~/dream/02-xcodefoundation✓
2. Install the Xcode Command Line Tools
Homebrew, git, and nearly every native build need Apple's compiler toolchain. You do not need all of Xcode for this.
xcode-select--install
The "xcode-select" command requires the command line developer tools. Would you like to install the tools now?
CancelInstall
Once the download finishes, you have:
clangmakegit
Click Install, then Agree, and let it finish before you move on.
xcode-select-pclang--versionmake--version
You should see/Library/Developer/CommandLineTools from the first command, and version banners from the other two.
~/dream/03-brewfoundation✓
3. Install Homebrew, exactly once
Almost everything from here on comes through Homebrew. Check first, because it may already be there.
The installer asks for your password and takes a few minutes. It ends by printing two lines. Here is the one trap in this step.
~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"eval "$(/opt/homebrew/bin/brew shellenv)"eval "$(/opt/homebrew/bin/brew shellenv)"eval "$(/opt/homebrew/bin/brew shellenv)"eval "$(/opt/homebrew/bin/brew shellenv)"...twenty copies on a real machine, one per re-runeval "$(/opt/homebrew/bin/brew shellenv)"
Appending that line every time you run the installer quietly slows every new shell. One copy is all you need.
You should see/opt/homebrew/bin/brew. Read what brew doctor reports, though most of its warnings about unrelated tools are safe to ignore.
Build the terminal
This is the room you will spend your days in, so it gets the most care.
~/dream/04-weztermterminal✓
4. Give the terminal a proper home
WezTerm is GPU-accelerated, configured in Lua, and pleasantly boring to keep running. Install it with the Nerd Font that the prompt and NvChad both rely on for their icons.
window_background_opacity = 0.8Enough glass to see the forest.
macos_window_background_blur = 10Enough blur to read your code.
window_paddingThe dashed gap. Text stops crowding the edge, and the edge is easier to grab.
RESIZE | MACOS_FORCE_ENABLE_SHADOWNo titlebar, but the native resize border stays.
#214969
#E52E2E
#44FFB1
#FFE073
#0FC5ED
#A277FF
#24EAF7
#CBE0F0
This whole page is set in those colors, and the panel you are reading sits at the same 0.8 opacity and blur 10. The wallpaper is your call, though a foggy forest is hard to beat.
Pick the right font name. NvChad warns against Nerd Font variants that end in "Mono" because their icons render small. Use JetBrainsMono Nerd Font, not JetBrainsMono Nerd Font Mono.
WezTerm reads ~/.wezterm.lua and reloads it the moment you save.
~/.wezterm.lua
[ -e ~/.wezterm.lua ] && mv ~/.wezterm.lua ~/.wezterm.lua.bak.$(date +%Y%m%d-%H%M%S)
cat > ~/.wezterm.lua <<'EOF'
local wezterm = require("wezterm")
local config = wezterm.config_builder()
config.font = wezterm.font("JetBrainsMono Nerd Font")
config.font_size = 15
-- Clean titlebar-less look that keeps the native resize border.
config.window_decorations = "RESIZE | MACOS_FORCE_ENABLE_SHADOW"
config.window_background_opacity = 0.8
config.macos_window_background_blur = 10
-- Breathing room, and an edge that is easier to aim for with the mouse.
config.window_padding = { left = 8, right = 12, top = 8, bottom = 12 }
-- Cmd-drag anywhere to move the borderless window.
config.mouse_bindings = {
{
event = { Drag = { streak = 1, button = "Left" } },
mods = "CMD",
action = wezterm.action.StartWindowDrag,
},
}
-- coolnight
config.colors = {
foreground = "#CBE0F0",
background = "#011423",
cursor_bg = "#47FF9C",
cursor_border = "#47FF9C",
cursor_fg = "#011423",
selection_bg = "#033259",
selection_fg = "#CBE0F0",
ansi = { "#214969", "#E52E2E", "#44FFB1", "#FFE073", "#0FC5ED", "#a277ff", "#24EAF7", "#24EAF7" },
brights = { "#214969", "#E52E2E", "#44FFB1", "#FFE073", "#A277FF", "#a277ff", "#24EAF7", "#24EAF7" },
}
return config
EOF
Open WezTerm from Applications and do the rest of this guide inside it. Then check that it can see the font:
If this is not a fresh Mac, move your old file aside first. Then write the new one. The language manager lines stay quiet until step 9 installs those tools, so you only do this once.
~/.zshrc
[ -e ~/.zshrc ] && mv ~/.zshrc ~/.zshrc.bak.$(date +%Y%m%d-%H%M%S)
cat > ~/.zshrc <<'EOF'# SDKMAN: Java, Kotlin, Gradle, Maven
export SDKMAN_DIR="$HOME/.sdkman"
[[ -s"$SDKMAN_DIR/bin/sdkman-init.sh" ]] && source "$SDKMAN_DIR/bin/sdkman-init.sh"# Prompt
source "$HOMEBREW_PREFIX/share/powerlevel10k/powerlevel10k.zsh-theme"
[[ -f ~/.p10k.zsh ]] && source ~/.p10k.zsh
# User-installed CLIs: pipx, Claude Code, and friends
export PATH="$HOME/.local/bin:$PATH"# History, shared across every open shell
HISTFILE=$HOME/.zhistory
SAVEHIST=10000
HISTSIZE=10000
setopt share_history hist_expire_dups_first hist_ignore_dups hist_verify
# Up and Down search history for what you have already typed
bindkey '^[[A' history-search-backward
bindkey '^[[B' history-search-forward
# Language managers. Each line waits quietly until step 9.
command -v fnm >/dev/null && eval "$(fnm env --use-on-cd)"
command -v pyenv >/dev/null && eval "$(pyenv init -)"
if [[ -f"$HOMEBREW_PREFIX/opt/chruby/share/chruby/chruby.sh" ]]; then
source "$HOMEBREW_PREFIX/opt/chruby/share/chruby/chruby.sh"
source "$HOMEBREW_PREFIX/opt/chruby/share/chruby/auto.sh"
[[ -d ~/.rubies ]] && chruby ruby-3.4
fi
# Plugins. Order matters: syntax highlighting must load last.
source "$HOMEBREW_PREFIX/share/zsh-autosuggestions/zsh-autosuggestions.zsh"
source "$HOMEBREW_PREFIX/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh"
export CLICOLOR=1
EOFexec zsh -l
The Powerlevel10k wizard starts on its own the first time. These answers give you the prompt in the picture below, and you can rerun it any time with p10k configure.
Style Lean, character set Unicode, height Two lines
Connection Dotted, frame Full, ornaments Lightest
Spacing Sparse, icons Few, flow Concise, instant prompt Off
Your public key is now on the clipboard. Paste it into GitHub, Settings, SSH and GPG keys, New SSH key, and do the same on any other host you use. Then test it:
ssh-T git@github.com
You should see a greeting with your username. GitHub adds that it does not provide shell access, which is normal.
While you are here, introduce yourself to git with some sensible defaults.
A current toolchain for native builds, beyond Apple's clang
nvim--version | head -1
tree-sitter--versionrg--version | head -1
git lfs version
"$(brew --prefix llvm)/bin/llvm-config"--version
Watch out: Homebrew keeps llvm off your PATH on purpose so it cannot shadow Apple's compiler. A bare llvm-config will say "command not found", and plain clang still means Apple's. That is why the check above spells out the full path.
NvChad wants a clean slate. Move any old Neovim config and data out of the way, then clone the starter.
for d in ~/.config/nvim ~/.local/share/nvim \
~/.local/state/nvim ~/.cache/nvim; do
[ -e"$d" ] && mv "$d""$d.bak"
done
git clone https://github.com/NvChad/starter ~/.config/nvim && nvim
Let lazy.nvim finish downloading plugins. Then, inside Neovim, run these one at a time:
:MasonInstallAll:TSInstallAll:Lazy sync:qa
Open nvim once more and run :MasonUpdate to refresh the package registry. Then detach the config from the starter repo so it is yours to change:
1package main
23import (
4"log"5"net/http"6)
78// main starts the API on :8080.9funcmain() {
10 mux := http.NewServeMux()
11registerRoutes(mux)
12 log.Fatal(http.ListenAndServe(":8080", mux))
13}
Live Grep
❯ registerRoutes
main.go:11routes.go:7routes_test.go:19
NORMALmain.godream-api11/13
:MasonUpdate
Where you end up. Spacefw opens ripgrep through Telescope, and Ctrln toggles the file tree.
~/dream/08-herdragents✓
8. Run a herd of agents with Herdr
Herdr is a terminal multiplexer built for AI coding agents. Think tmux with a sidebar that tells you which agent is working, which one is stuck waiting for you, and which one has finished. Sessions keep running when you close the window.
Any agent works. Herdr detects claude, codex, opencode, and others on its own when you start them in a pane.
Open a new terminal window so claude lands on your PATH, then check both tools and connect them. The integration adds native session restore for Claude.
claude--versionclaude doctor
herdr integration install claude
herdr integration status
Now go to a project, run herdr, and start an agent in a pane.
dream-apiclaude workingzsh idle
ios-appclaude blocked
docscodex done
●claude Reading routes.go Adding a health check Edit internal/health.go + func Health(w, r) Bash go test ./... okdream-api/internal Working█
╰─❯nvim . main.go routes.go
╰─❯go run ./cmd/api listening on :8080
An illustration of the layout. The sidebar is the point: one glance shows that ios-app needs you and the other two do not.
The prefix is Ctrlb, the same as tmux. Press it, then one of these.
v Split right
- Split down
c New tab
n / p Next or previous tab
w Move between workspaces
Shiftn New workspace
q Detach, agents keep running
? Show every binding
Run herdr again to reattach. herdr server stop ends the session and stops its panes. The mouse works too: click to focus, drag borders to resize, right-click to split.
Finish the machine
The last stretch: languages, heavier IDEs, system settings, secrets, and apps.
~/dream/09-langslanguages✓
9. One manager per language
Install runtimes through Homebrew and you eventually get the classic "wrong version on PATH" afternoon. Each language gets its own manager here, so each project can pin what it needs.
Language
Manager
Pin
Project file
Node.js
fnm
latest LTS
.nvmrc
Python
pyenv + pipx
3.12
.python-version
Ruby
chruby + ruby-install
3.4
.ruby-version
Java and Kotlin
SDKMAN
21 LTS
.sdkmanrc
Rust
rustup
stable
rust-toolchain.toml
Go
Homebrew
latest
go.mod
Dart and Flutter
Flutter SDK
stable
pubspec.yaml
Go is the only language Homebrew manages here. Its toolchain updates cleanly in place. These pins are starting points, so swap in whatever your projects target.
Install only what you use. The ~/.zshrc from step 5 already knows about all of them.
curl--proto'=https'--tlsv1.2 -sSf https://sh.rustup.rs | sh
source"$HOME/.cargo/env"rustc--versionbrew install go
go version
Dart and Flutter
Skip standalone Dart, since Flutter ships a matching SDK. flutter doctor walks you through whatever is still missing.
brew install --cask flutter
flutter doctor
~/dream/10-idesides✓
10. Add the heavyweight IDEs
Neovim handles most days. These handle the rest. Install only the ones your work calls for.
Visual Studio Code
brew install --cask visual-studio-code
In VS Code, open the Command Palette with CmdShiftP and run Shell Command: Install 'code' command in PATH. Then either sign in to the built-in Settings Sync, or move your extensions by hand:
# On the old Maccode--list-extensions > extensions.txt
# On the new Macxargs-n 1 code --install-extension < extensions.txt
Install the Toolbox, then let it manage IntelliJ IDEA, Android Studio, and the rest. JetBrains keeps settings in its in-app Settings Sync, so there are no dotfiles to carry.
brew install --cask jetbrains-toolbox
Xcode
For iOS, macOS, or Swift work, install Xcode from the Mac App Store, then:
Stock macOS is set up for typing letters. Hold j in Neovim and you will feel the difference at once. Try it here, at the real speeds.
Stock macOS375 ms, then 90 ms
0
After this step225 ms, then 30 ms
0
Over one second that works out to 8 characters against 27. Values count in 15 ms ticks, so InitialKeyRepeat 15 is 225 ms and KeyRepeat 2 is 30 ms.
This only changes settings that affect how you work. Dock size, wallpaper, dark mode, and trackpad speed are yours to set by hand. Read it first and delete any line you disagree with.
osascript-e'tell application "System Settings" to quit'# Keyboard: fast repeat, and no autocorrect mangling your codedefaults write NSGlobalDomain InitialKeyRepeat -int 15
defaults write NSGlobalDomain KeyRepeat -int 2
defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool false
defaults write NSGlobalDomain NSAutomaticCapitalizationEnabled -bool false
defaults write NSGlobalDomain NSAutomaticDashSubstitutionEnabled -bool false
defaults write NSGlobalDomain NSAutomaticPeriodSubstitutionEnabled -bool false
defaults write NSGlobalDomain NSAutomaticQuoteSubstitutionEnabled -bool false
defaults write NSGlobalDomain NSAutomaticSpellingCorrectionEnabled -bool false
# Finder: show the truthdefaults write NSGlobalDomain AppleShowAllExtensions -bool true
defaults write com.apple.finder AppleShowAllFiles -bool true
defaults write com.apple.finder ShowPathbar -bool true
defaults write com.apple.finder ShowStatusBar -bool true
defaults write com.apple.finder _FXShowPosixPathInTitle -bool true
defaults write com.apple.finder NewWindowTarget -string"PfHm"defaults write com.apple.finder NewWindowTargetPath -string"file://${HOME}/"defaults write com.apple.finder FXDefaultSearchScope -string"SCcf"defaults write com.apple.finder FXEnableExtensionChangeWarning -bool false
# No .DS_Store litter on network or USB drivesdefaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true
defaults write com.apple.desktopservices DSDontWriteUSBStores -bool true
# Screenshots: their own folder, PNG, no window shadowmkdir-p"${HOME}/Pictures/Screenshots"defaults write com.apple.screencapture location -string"${HOME}/Pictures/Screenshots"defaults write com.apple.screencapture type -string"png"defaults write com.apple.screencapture disable-shadow -bool true
# Spaces stay where you put them, and Mission Control hurries updefaults write com.apple.dock mru-spaces -bool false
defaults write com.apple.dock expose-animation-duration -float 0.12
killall Finder Dock SystemUIServer
Log out and back in for the key repeat to take effect. Everything here is safe to run again, and any setting comes back with defaults delete, for example defaults delete NSGlobalDomain KeyRepeat.
Safari is deliberately missing. Modern macOS protects Safari's preferences, so scripted writes tend to fail. Turn it on by hand instead: Safari, Settings, Advanced, Show features for web developers.
~/dream/12-secretssecrets✓
12. Put your dotfiles and secrets under lock
Your config is now worth keeping. Put it in a private repo, and let git-crypt encrypt the few files that hold real tokens, so the next Mac takes minutes instead of a weekend.
Your working treePlain text. Your editor and tools read it normally.
//registry...:_authToken=abc
git push
The remoteCiphertext only. A leak of the repo leaks nothing useful.
.GITCRYPT.8f3ac91d07e2b54f
git-crypt unlock
Your next MacYour GPG key turns it back into plain text.
//registry...:_authToken=abc
Encryption happens inside git, on the way in and out. You keep working with normal files.
First, make a GPG key. Pick ECC or RSA 4096, and use the same email as your git commits.
The long ID follows the slash on the sec line. Create an empty private repo on your git host, then set up the local copy. The .gitattributes rule must be committed before any secret goes in.
KEY=ABCDEF1234567890 # your long key IDmkdir-p ~/macos-env-setup/dotfiles ~/macos-env-setup/secrets
cd ~/macos-env-setup
git init
git-crypt init
printf'secrets/** filter=git-crypt diff=git-crypt\n' > .gitattributes
git add .gitattributes
git commit -m"Encrypt everything under secrets/"git-crypt add-gpg-user "$KEY"cp ~/.zshrc ~/.zprofile ~/.p10k.zsh ~/.wezterm.lua ~/.gitconfig dotfiles/
cp ~/.netrc ~/.npmrc secrets/ 2>/dev/null
git add dotfiles secrets
git-crypt status
Stop and check: every file under secrets/ must say encrypted before you commit. Plain text that reaches git history stays there, and the only fix is to rotate the token and rewrite history.
git commit -m"Add dotfiles and encrypted secrets"git remote add origin git@github.com:you/macos-env-setup.git
git push -u origin main
Back up the private GPG key to your password manager. Without it, nothing unlocks.
gpg--export-secret-keys--armor"$KEY" > ~/Desktop/gpg-key.asc
# Save that file to your password manager, then:rm ~/Desktop/gpg-key.asc
Bring or regenerate? If a token is easy to mint again, mint a fresh one. The encrypted folder is for things that are painful to recreate, not for everything.
~/dream/13-appsapps✓
13. Install the apps, in the right order
The order is the trick. Each group makes the next one easier: a browser so you can sign in, then a password manager so the sign-ins are painless, and only then the rest.
Leave yourself time for the apps that need a paid sign-in or a manual step, such as Adobe Creative Cloud, Microsoft 365, and your JetBrains account. Keep this list honest, too. If you have not opened an app in a year, it does not get a place on the new machine.
Check the whole trail
Paste this into a fresh WezTerm window. Green ticks only, please.
for tool in brew nvim rg tree-sitter herdr claude fnm pyenv \
pipx chruby sdk rustc go gpg git-crypt; do
if command -v"$tool" >/dev/null; then
print-P"%F{green}✓%f $tool"
else
print-P"%F{red}✗%f $tool"
fi
done
To keep it all fresh, run this every week or two. Inside Neovim, :Lazy sync and :MasonUpdate do the same job for your plugins.