Skip to content

Local (macOS) setup

Your laptop is the default dev box. The optional always-on box lives at VPS setup, and herdr gets its own page at herdr.

Tested on Apple Silicon (Homebrew at /opt/homebrew). On Intel Macs every /opt/homebrew path below becomes /usr/local. Where a config block is shared with the VPS guide (zshrc, Starship, tmux, beads), the versions here are the same ones, adapted for Homebrew paths.

Throughout this guide, replace these placeholders:

  • <repo>: the directory name a repo lives in (for example my-project)
  • <repo-url>: its SSH clone URL (for example git@github.com:<you>/my-project.git)
  • <vps-tailscale-ip>: a VPS’s Tailscale IP (from tailscale ip -4 on that box)

End state:

  • Homebrew with the carry-forward package set installed
  • zsh with Starship prompt, autosuggestions, syntax highlighting, history search
  • LANG guarded in .zshrc so launchd-spawned shells render prompts correctly
  • bun, uv and pnpm all present, with clear rules for which to reach for
  • Claude Code authenticated, with the cc / ccc / cx launchers
  • herdr running as a background service, config at ~/.config/herdr/config.toml
  • Beads (bd) available for any repo that uses it
  • Optional: Tailscale, so this Mac can reach VPS boxes
  • macOS with Command Line Tools (xcode-select --install)
  • A GitHub account, and an SSH key you can register with it
  • An Anthropic account for Claude Code

Terminal window
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

The installer prints two eval lines at the end to put brew on PATH. Ignore them: the zshrc in step 4 puts /opt/homebrew/bin on PATH directly, which is cheaper than running brew shellenv on every shell start. For this session only:

Terminal window
export PATH="/opt/homebrew/bin:$PATH"
brew --version

These are the packages that earn their place on every machine. Everything else is situational; see the appendix at the end.

Terminal window
brew install \
starship zsh-autosuggestions zsh-syntax-highlighting tmux \
gh beads herdr \
uv pnpm \
jq fd just

What each is for:

Package Why
starship Prompt, config in step 5
zsh-autosuggestions Ghost-text completion from history
zsh-syntax-highlighting Colours the command line as you type
tmux Session persistence, config in step 6
gh GitHub CLI: PRs, issues, gh ssh-key add
beads bd issue tracker (step 9)
herdr Terminal agent multiplexer (step 8)
uv The only Python tool you need
pnpm Node package manager for npm-shaped ecosystems
jq JSON on the command line, including inside shell hooks
fd Fast find replacement
just Task runner for per-repo command recipes

Authenticate gh now, since later steps assume it:

Terminal window
gh auth login # choose SSH, let it upload a key if you have none
gh auth status

Install via the official installer rather than Homebrew:

Terminal window
curl -fsSL https://bun.sh/install | bash

That lands in ~/.bun/bin, which the zshrc in step 4 already has on PATH. bun upgrade then handles updates in place, which is faster than waiting for a formula bump.

Installed by brew in step 2. The rule is absolute: always uv, never pip, python -m venv, or virtualenv.

Terminal window
uv init <project> # new project, writes pyproject.toml
uv add <package> # instead of pip install
uv sync # install from pyproject.toml + uv.lock
uv run python x.py # run inside the project env
uvx <tool> # one-off tool run, no install

If something wants a requirements.txt, generate a pyproject.toml instead.

Never npm, npx, npm install, npm run, and never generate a package-lock.json. That applies inside scripts, READMEs and build steps too. Pick between pnpm and bun per project, and state which and why at the top of the work:

  • bun when build speed matters most, the dependency tree is small and modern, and nothing in the ecosystem demands npm semantics. bun runs TypeScript natively and is dramatically faster for installs and scripts.
  • pnpm when the ecosystem expects npm semantics: Obsidian plugins, Rollup/Vite/Webpack setups with many plugins, github: dependencies, anything where reproducibility for collaborators outranks raw speed.

Commit bun.lock or pnpm-lock.yaml. Never commit package-lock.json; if you find one in an existing project, flag it and propose migrating.


macOS ships zsh already, so there is no chsh step. Write this as ~/.zshrc:

Terminal window
cat > "$HOME/.zshrc" << 'ZSHRC'
# --- Locale (must come first; see the launchd caution below) ---
export LANG="${LANG:-en_GB.UTF-8}"
# --- PATH ---
export PATH="/opt/homebrew/bin:$HOME/bin:$HOME/.bun/bin:$HOME/.local/bin:$PATH"
# --- Editor ---
export EDITOR='subl'
# --- History ---
HISTFILE="$HOME/.zsh_history"
HISTSIZE=50000
SAVEHIST=50000
setopt EXTENDED_HISTORY
setopt HIST_EXPIRE_DUPS_FIRST
setopt HIST_IGNORE_DUPS
setopt HIST_IGNORE_SPACE
setopt SHARE_HISTORY
setopt APPEND_HISTORY
setopt INC_APPEND_HISTORY
# Up/Down arrow searches history filtered by what you've already typed
autoload -U up-line-or-beginning-search down-line-or-beginning-search
zle -N up-line-or-beginning-search
zle -N down-line-or-beginning-search
bindkey "^[[A" up-line-or-beginning-search
bindkey "^[[B" down-line-or-beginning-search
# app-cursor variants, so the same keys work inside tmux and full-screen apps
bindkey "^[OA" up-line-or-beginning-search
bindkey "^[OB" down-line-or-beginning-search
# --- Completions ---
autoload -U compinit && compinit
autoload -U bashcompinit && bashcompinit
# --- Plugins (Homebrew) ---
source /opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zsh
source /opt/homebrew/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
# --- pnpm ---
export PNPM_HOME="$HOME/Library/pnpm"
case ":$PATH:" in
*":$PNPM_HOME:"*) ;;
*) export PATH="$PNPM_HOME:$PATH" ;;
esac
# --- uv ---
eval "$(uv generate-shell-completion zsh)"
eval "$(uvx --generate-shell-completion zsh)"
# --- Claude Code ---
cc() { printf "\e]0;CC: ${PWD/#$HOME/~}\a"; claude --dangerously-skip-permissions "$@"; }
ccc() { cc -c "$@"; }
ccr() { cc -r "$@"; }
cca() { cc agents "$@"; }
alias cx="codex --dangerously-bypass-approvals-and-sandbox"
alias cl="clear"
# --- Terminal title follows cwd ---
precmd() { print -Pn "\e]0;%~\a" }
# --- Prompt ---
eval "$(starship init zsh)"
ZSHRC

Reload with exec zsh. The cc launcher skips permission prompts, which is a real trade rather than a free speedup; Claude Code workflow covers what that gives up.

Things deliberately not in this zshrc, and why:

  • Android SDK, JAVA_HOME, Maestro paths. Only relevant on a machine doing mobile work; add them in a separate ~/.zshrc.local sourced at the end, so a rebuild does not inherit them.
  • Per-project PATH entries (a repo’s .venv/bin, a tool directory inside one checkout). These belong in that repo’s .envrc or justfile, not in the global shell.
  • Anything hardcoding an absolute path into one repo. A launcher that points at a file inside a specific checkout breaks the moment that repo moves. Define those per machine, outside the shared config.
  • Commented-out OAuth tokens. A CLAUDE_CODE_OAUTH_TOKEN sitting commented out in a dotfile is a secret one git add away from a public repo. Keep tokens in the keychain or in ~/.claude/.credentials.json, which claude auth login manages for you.
  • nvm. Sourcing nvm costs a few hundred milliseconds on every shell start. If a project needs a pinned Node, prefer node@22 from Homebrew or bun’s own runtime; reach for nvm only when you genuinely juggle Node versions.

This is the same config as the VPS, so both prompts match. Write it to ~/.config/starship.toml:

Terminal window
mkdir -p "$HOME/.config"
cat > "$HOME/.config/starship.toml" << 'STARSHIP'
add_newline = false
# A minimal left prompt
format = """$directory$character"""
palette = "catppuccin_mocha"
# move the rest of the prompt to the right with explicit git_branch inclusion
right_format = """$git_branch$all"""
command_timeout = 1000
[character]
vicmd_symbol = '[N >>>](bold yellow)'
success_symbol = '[>](bold green)'
# Shorten directories you visit constantly (optional, edit to taste)
[directory.substitutions]
'~/code/very-long-project-name' = 'proj'
[git_branch]
format = '[$symbol$branch(:$remote_branch)]($style) '
style = "bold purple"
[aws]
format = '[$symbol(profile: "$profile" )(\(region: $region\) )]($style)'
disabled = false
style = 'bold blue'
symbol = " "
[golang]
format = '[ ](bold cyan)'
[kubernetes]
symbol = '☸ '
disabled = true
detect_files = ['Dockerfile']
format = '[$symbol$context( \($namespace\))]($style) '
[gcloud]
format = '[$symbol]($style) '
symbol = '☁️'
[docker_context]
disabled = true
[bun]
disabled = true
[palettes.catppuccin_mocha]
rosewater = "#f5e0dc"
flamingo = "#f2cdcd"
pink = "#f5c2e7"
mauve = "#cba6f7"
red = "#f38ba8"
maroon = "#eba0ac"
peach = "#fab387"
yellow = "#f9e2af"
green = "#a6e3a1"
teal = "#94e2d5"
sky = "#89dceb"
sapphire = "#74c7ec"
blue = "#89b4fa"
lavender = "#b4befe"
text = "#cdd6f4"
subtext1 = "#bac2de"
subtext0 = "#a6adc8"
overlay2 = "#9399b2"
overlay1 = "#7f849c"
overlay0 = "#6c7086"
surface2 = "#585b70"
surface1 = "#45475a"
surface0 = "#313244"
base = "#1e1e2e"
mantle = "#181825"
crust = "#11111b"
STARSHIP

Verify both prompt modes render without a WARN from the character module:

Terminal window
STARSHIP_SHELL=zsh starship prompt --status 0
STARSHIP_SHELL=zsh starship prompt --keymap vicmd --status 0

A Nerd Font is required for the glyphs to render (Ghostty and iTerm2 both ship one; in Terminal.app you have to install and select one yourself).


Same file as the VPS:

Terminal window
cat > "$HOME/.tmux.conf" << 'TMUX'
unbind C-b
set -g prefix §
bind § send-prefix
bind C-b send-prefix
set -g mouse on
# Enter copy mode and scroll with PgUp/PgDn
bind -n Pageup copy-mode -u
bind -n Pagedown send-keys Pagedown
bind -T copy-mode Pageup send -X page-up
bind -T copy-mode Pagedown send -X page-down
bind -T copy-mode-vi Pageup send -X page-up
bind -T copy-mode-vi Pagedown send -X page-down
# Escape to exit copy mode fast
bind -T copy-mode Escape send -X cancel
bind -T copy-mode-vi Escape send -X cancel
TMUX

Reload a running server with tmux source-file ~/.tmux.conf.


Terminal window
curl -fsSL https://claude.ai/install.sh | bash
claude --version # 2.x.x
claude auth login # interactive, opens a browser

The installer puts claude in ~/.local/bin, already on PATH from step 4.

Launch with cc (or ccc to continue, ccr to resume) from step 4. Day-to-day usage is covered in Claude Code workflow.


herdr is the terminal agent multiplexer: one server process, many agent panes. See herdr for what it does and how to drive panes from scripts.

Terminal window
brew install herdr # already covered by step 2
herdr --version

Run the server one of two ways:

Terminal window
# Background, managed by launchd, survives logout and reboots
brew services start herdr
brew services list | grep herdr # should read "started"
# Or foreground, when you want to watch it or are debugging startup
herdr serve

Config lives at ~/.config/herdr/config.toml. A reasonable starting point, matching the tmux prefix from step 6:

onboarding = false
[ui]
sidebar_width = 26
sidebar_min_width = 26
sidebar_max_width = 26
agent_panel_sort = "priority"
[keys]
prefix = "§"

Logs and runtime state sit alongside it: herdr-server.log, herdr-client.log, session.json, and the two Unix sockets. When something looks wrong, tail -f ~/.config/herdr/herdr-server.log is the first stop.


Beads (bd) tracks issues in a Dolt database synced over refs/dolt/data on the repo’s git remote. A repo that uses beads has .beads/config.yaml, metadata.json and issues.jsonl git-tracked, so cloning it brings the config; this step wires up the actual database.

beads came from Homebrew in step 2. The VPS guide uses the upstream install script instead, because there is no Homebrew there:

Terminal window
# already done on this Mac via step 2
brew install beads
# the VPS/Linux equivalent, for reference
curl -sSL https://raw.githubusercontent.com/gastownhall/beads/main/scripts/install.sh | bash

Then, inside a repo that uses beads:

Terminal window
bd --version # confirm it matches every other machine on this project
cd ~/<repo>
bd bootstrap --yes # clones the shared Dolt database from the git remote
bd stats # sanity check: counts should match your other machines
git config beads.role maintainer # match the role used on other dev machines for this repo

Needed only if this Mac has to reach VPS boxes whose SSH is locked to the tailnet.

Terminal window
brew install --cask tailscale
# or, if you prefer the sandboxed App Store build (needs the `mas` CLI):
mas install 1475387142

Launch Tailscale.app once and sign in to the tailnet. The GUI build hides the CLI inside the bundle, so symlink it if you want tailscale on PATH:

Terminal window
sudo ln -sf /Applications/Tailscale.app/Contents/MacOS/Tailscale /opt/homebrew/bin/tailscale
tailscale status

For the ~/.ssh/config host alias (the RemoteCommand pattern that drops you straight into ~/<repo> on the VPS, plus the RemoteForward 52698 line for rsub remote editing), see the VPS setup guide. That page is the canonical copy; do not fork it here.


Useful Homebrew packages grouped by why you would want them. None of these are needed for a working machine, so install per project rather than up front.

Shell and general CLI

Terminal window
brew install coreutils bash stow watch pwgen macos-trash mas blueutil

coreutils gives GNU versions (gls, gdate) that behave like the ones in every Linux script you will paste. stow manages dotfile symlinks. mas drives the Mac App Store from the CLI. blueutil toggles Bluetooth from a script.

Code quality and CI

Terminal window
brew install actionlint shfmt ast-grep zizmor

actionlint and zizmor lint GitHub Actions workflows (the latter for security issues specifically). ast-grep does structural search and replace across a codebase, which is far safer than regex for refactors.

Cloud and infra

Terminal window
brew install awscli aws-sam-cli ansible go openjdk

Databases and local services

Terminal window
brew install postgresql@14 mysql supabase temporal

Each of these can run under brew services, so the same launchd LANG caveat from step 8 applies to anything they spawn.

Media and file wrangling

Terminal window
brew install ffmpeg poppler pngpaste gallery-dl

poppler supplies pdftotext and pdfimages. pngpaste writes the clipboard image to a file, which is handy for feeding screenshots to an agent.

iOS and mobile

Terminal window
brew install cocoapods xcodegen swiftlint ideviceinstaller

Networking

Terminal window
brew install nmap

Language extras

Terminal window
brew install pipx node@22 nvm

pipx is largely superseded by uv tool install. nvm costs shell startup time (see step 4); prefer node@22 unless you truly need to switch versions.

Agent tooling

Terminal window
brew install opencode

Deliberately absent from both lists: antigen and powerlevel10k (both superseded by the direct plugin sourcing and Starship in steps 4 and 5), ffmpeg-full (a duplicate of ffmpeg) and neofetch (abandoned upstream).


  • LANG first, always. It is the highest-leverage line in the zshrc and the cause of the weirdest-looking bug on the machine. Anything you add above it that prints or measures output can reintroduce the problem.
  • Never npm. Choose pnpm or bun explicitly, and say which and why at the top of the work.
  • Never pip. uv covers install, venv, run and one-off tools.
  • Homebrew services mean launchd. Anything under brew services inherits a stripped environment. When a background service behaves differently from the same command in your terminal, compare env output before debugging anything else.
  • Casks are not covered here. GUI apps (terminal, editors, browsers, password managers, launchers) are a personal-preference layer, and installing them is a brew install --cask away once the shell works.

Next: Claude Code workflow for driving the agent, herdr once you run more than one at a time, and VPS setup if you want an always-on box.