How to SetupDream SWE DevEnvironmenton macOS

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.

Where this path ends

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

  1. No bootstrap script. Each step is something you choose to run. Read it, paste it, check it, move on.
  2. Pin major versions, allow minor drift. A project's own version file always wins.
  3. One manager per language. Homebrew does not get to own your runtimes, with Go as the one exception.
  4. 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.

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 Intel
sw_vers        # your macOS version
echo $SHELL    # /bin/zsh on any modern Mac
arm64
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.

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

Once the download finishes, you have:

clangmakegit
Click Install, then Agree, and let it finish before you move on.
xcode-select -p
clang --version
make --version

You should see /Library/Developer/CommandLineTools from the first command, and version banners from the other two.

3. Install Homebrew, exactly once

Almost everything from here on comes through Homebrew. Check first, because it may already be there.

brew --version || /bin/bash -c \
  "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

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
Appending that line every time you run the installer quietly slows every new shell. One copy is all you need.

So add the line only if it is not already there:

grep -qs 'brew shellenv' ~/.zprofile ||
  echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"

which brew
brew doctor

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.

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.

brew install --cask wezterm font-jetbrains-mono-nerd-font
  • 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.
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:

wezterm ls-fonts --list-system | grep -i "JetBrainsMono Nerd"

5. Make the shell feel like yours

No Oh My Zsh and no framework. Stock zsh plus three Homebrew packages gets you everything you will notice.

brew install powerlevel10k zsh-autosuggestions zsh-syntax-highlighting

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
EOF

exec 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
1: zsh ×
  • the pathWhere you are, with the last folder in bold.
  • the branchGit status at a glance, without typing it.
  • the timerShown only when the last command took a while.
  • the grey textAutosuggestion from your history. Press → to take it.

6. Give this Mac its own keys

Generate a fresh key for every machine instead of copying an old one around. If a laptop goes missing, you revoke one key and nothing else changes.

Private keyStays here, with its passphrase in your Keychain.
~/.ssh/id_ed25519
public half only
Public keySafe to share, so paste it anywhere you push code.
~/.ssh/id_ed25519.pub
github.combitbucket.orggitlab.com
Only the public half ever leaves this Mac. Private keys stay out of every repo, even encrypted ones.
ssh-keygen -t ed25519 -C "you@example.com (macbook 2026-11)"
ssh-add --apple-use-keychain ~/.ssh/id_ed25519

cat >> ~/.ssh/config <<'EOF'
Host *
  AddKeysToAgent yes
  UseKeychain yes
  IdentityFile ~/.ssh/id_ed25519
EOF
chmod 600 ~/.ssh/config

pbcopy < ~/.ssh/id_ed25519.pub

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.

git config --global user.name  "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
git config --global pull.rebase true
git config --global http.postBuffer 157286400
git config --global core.excludesfile ~/.gitignore_global
printf '.DS_Store\n.env\n*.swp\n' > ~/.gitignore_global

Bring in the editor and the agents

Neovim is where you write. Herdr is where you direct everything that writes alongside you.

7. Install Neovim and NvChad

Start with the systems hacker package set. None of these are random dependencies.

brew install neovim tree-sitter-cli ripgrep git-lfs llvm
git lfs install
PackageWhy it is here
neovimNvChad needs version 0.11 or later
tree-sitter-cliRequired to build syntax parsers
ripgrepPowers project-wide search in Telescope
git-lfsHandles the large binary files some repos carry
llvmA current toolchain for native builds, beyond Apple's clang
nvim --version | head -1
tree-sitter --version
rg --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:

rm -rf ~/.config/nvim/.git
Where you end up. Space f w opens ripgrep through Telescope, and Ctrl n toggles the file tree.

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.

brew install herdr
curl -fsSL https://claude.ai/install.sh | bash

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 --version
claude doctor
herdr integration install claude
herdr integration status

Now go to a project, run herdr, and start an agent in a pane.

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 Ctrl b, 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
  • Shift n 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.

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.

LanguageManagerPinProject file
Node.jsfnmlatest LTS.nvmrc
Pythonpyenv + pipx3.12.python-version
Rubychruby + ruby-install3.4.ruby-version
Java and KotlinSDKMAN21 LTS.sdkmanrc
Rustrustupstablerust-toolchain.toml
GoHomebrewlatestgo.mod
Dart and FlutterFlutter SDKstablepubspec.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.

Node.js

brew install fnm
exec zsh -l
fnm install --lts
fnm default lts-latest
fnm use default
node --version

Python

brew install pyenv pipx
brew install openssl@3 readline sqlite3 xz tcl-tk@8 libb2 zstd zlib pkgconfig
pipx ensurepath
exec zsh -l
pyenv install 3.12
pyenv global "$(pyenv latest 3.12)"
pipx install poetry ruff ipython httpie
python --version

Why the long list? pyenv compiles Python from source, and those libraries are what give you working ssl, sqlite3, and tkinter modules.

Ruby

brew install chruby ruby-install
ruby-install ruby 3.4
exec zsh -l
ruby -v
which ruby

You should see a path under ~/.rubies, not /usr/bin/ruby. That older system Ruby belongs to Apple, so leave it alone.

Java, Kotlin, Gradle, and Maven

curl -s "https://get.sdkman.io" | bash
exec zsh -l
sdk install java "$(sdk list java | grep -Eo '21\.[0-9.+]+-tem' | head -1)"
sdk install gradle
sdk install maven
sdk install kotlin

Rust and Go

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
rustc --version

brew 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

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 Cmd Shift P 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 Mac
code --list-extensions > extensions.txt

# On the new Mac
xargs -n 1 code --install-extension < extensions.txt

A few settings worth keeping everywhere:

settings.json
{
  "editor.rulers": [120],
  "editor.minimap.enabled": false,
  "files.trimTrailingWhitespace": true,
  "files.insertFinalNewline": true,
  "explorer.compactFolders": false
}

JetBrains

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:

sudo xcode-select -s /Applications/Xcode.app
sudo xcodebuild -license accept
xcodebuild -downloadPlatform iOS

11. Tune macOS for typing code

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 code
defaults 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 truth
defaults 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 drives
defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true
defaults write com.apple.desktopservices DSDontWriteUSBStores -bool true

# Screenshots: their own folder, PNG, no window shadow
mkdir -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 up
defaults 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.

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.

brew install gnupg git-crypt
gpg --full-generate-key
gpg --list-secret-keys --keyid-format=long

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 ID

mkdir -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

On your next Mac

brew install gnupg git-crypt
gpg --import ~/path/to/gpg-key.asc

git clone git@github.com:you/macos-env-setup.git ~/macos-env-setup
cd ~/macos-env-setup
git-crypt unlock

cp secrets/.netrc ~/.netrc && chmod 600 ~/.netrc
cp secrets/.npmrc ~/.npmrc && chmod 600 ~/.npmrc

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.

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.

brew install --cask google-chrome firefox              # 1
brew install --cask nordpass                           # 2, or 1password
brew install --cask docker-desktop                     # 3
brew install --cask tableplus postgres-app pgadmin4    # 4
brew install --cask postman insomnia tailscale-app     # 5

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.

brew update && brew upgrade && brew cleanup
rustup update

And whenever you add something you love, refresh your inventory so the next Mac gets it too:

cd ~/macos-env-setup
brew leaves > brew-formulae.txt
brew list --cask > brew-casks.txt

You made it out of the woods

It is morning, your terminal looks like a dream, and every tool answers on the first try. Go build the unicorn.

An engraved unicorn standing in the clearing at dawn