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 examplemy-project)<repo-url>: its SSH clone URL (for examplegit@github.com:<you>/my-project.git)<vps-tailscale-ip>: a VPS’s Tailscale IP (fromtailscale ip -4on that box)
End state:
- Homebrew with the carry-forward package set installed
- zsh with Starship prompt, autosuggestions, syntax highlighting, history search
LANGguarded in.zshrcso 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/cxlaunchers - 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
Prerequisites
Section titled “Prerequisites”- 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
1. Homebrew
Section titled “1. Homebrew”/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:
export PATH="/opt/homebrew/bin:$PATH"brew --version2. Carry-forward package set
Section titled “2. Carry-forward package set”These are the packages that earn their place on every machine. Everything else is situational; see the appendix at the end.
brew install \ starship zsh-autosuggestions zsh-syntax-highlighting tmux \ gh beads herdr \ uv pnpm \ jq fd justWhat 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:
gh auth login # choose SSH, let it upload a key if you have nonegh auth status3. bun, uv and pnpm
Section titled “3. bun, uv and pnpm”Install via the official installer rather than Homebrew:
curl -fsSL https://bun.sh/install | bashThat 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.
uv (Python)
Section titled “uv (Python)”Installed by brew in step 2. The rule is absolute: always uv, never pip, python -m venv, or virtualenv.
uv init <project> # new project, writes pyproject.tomluv add <package> # instead of pip installuv sync # install from pyproject.toml + uv.lockuv run python x.py # run inside the project envuvx <tool> # one-off tool run, no installIf something wants a requirements.txt, generate a pyproject.toml instead.
pnpm vs bun (Node)
Section titled “pnpm vs bun (Node)”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.
4. zshrc
Section titled “4. zshrc”macOS ships zsh already, so there is no chsh step. Write this as ~/.zshrc:
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=50000SAVEHIST=50000setopt EXTENDED_HISTORYsetopt HIST_EXPIRE_DUPS_FIRSTsetopt HIST_IGNORE_DUPSsetopt HIST_IGNORE_SPACEsetopt SHARE_HISTORYsetopt APPEND_HISTORYsetopt INC_APPEND_HISTORY
# Up/Down arrow searches history filtered by what you've already typedautoload -U up-line-or-beginning-search down-line-or-beginning-searchzle -N up-line-or-beginning-searchzle -N down-line-or-beginning-searchbindkey "^[[A" up-line-or-beginning-searchbindkey "^[[B" down-line-or-beginning-search# app-cursor variants, so the same keys work inside tmux and full-screen appsbindkey "^[OA" up-line-or-beginning-searchbindkey "^[OB" down-line-or-beginning-search
# --- Completions ---autoload -U compinit && compinitautoload -U bashcompinit && bashcompinit
# --- Plugins (Homebrew) ---source /opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zshsource /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)"ZSHRCReload 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.localsourced at the end, so a rebuild does not inherit them. - Per-project
PATHentries (a repo’s.venv/bin, a tool directory inside one checkout). These belong in that repo’s.envrcorjustfile, 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_TOKENsitting commented out in a dotfile is a secret onegit addaway from a public repo. Keep tokens in the keychain or in~/.claude/.credentials.json, whichclaude auth loginmanages for you. nvm. Sourcing nvm costs a few hundred milliseconds on every shell start. If a project needs a pinned Node, prefernode@22from Homebrew or bun’s own runtime; reach for nvm only when you genuinely juggle Node versions.
5. Starship config
Section titled “5. Starship config”This is the same config as the VPS, so both prompts match. Write it to ~/.config/starship.toml:
mkdir -p "$HOME/.config"cat > "$HOME/.config/starship.toml" << 'STARSHIP'add_newline = false# A minimal left promptformat = """$directory$character"""palette = "catppuccin_mocha"# move the rest of the prompt to the right with explicit git_branch inclusionright_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 = falsestyle = 'bold blue'symbol = " "
[golang]format = '[ ](bold cyan)'
[kubernetes]symbol = '☸ 'disabled = truedetect_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"STARSHIPVerify both prompt modes render without a WARN from the character module:
STARSHIP_SHELL=zsh starship prompt --status 0STARSHIP_SHELL=zsh starship prompt --keymap vicmd --status 0A 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).
6. tmux config
Section titled “6. tmux config”Same file as the VPS:
cat > "$HOME/.tmux.conf" << 'TMUX'unbind C-bset -g prefix §bind § send-prefixbind C-b send-prefixset -g mouse on
# Enter copy mode and scroll with PgUp/PgDnbind -n Pageup copy-mode -ubind -n Pagedown send-keys Pagedownbind -T copy-mode Pageup send -X page-upbind -T copy-mode Pagedown send -X page-downbind -T copy-mode-vi Pageup send -X page-upbind -T copy-mode-vi Pagedown send -X page-down
# Escape to exit copy mode fastbind -T copy-mode Escape send -X cancelbind -T copy-mode-vi Escape send -X cancelTMUXReload a running server with tmux source-file ~/.tmux.conf.
7. Claude Code
Section titled “7. Claude Code”curl -fsSL https://claude.ai/install.sh | bashclaude --version # 2.x.xclaude auth login # interactive, opens a browserThe 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.
8. herdr
Section titled “8. herdr”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.
brew install herdr # already covered by step 2herdr --versionRun the server one of two ways:
# Background, managed by launchd, survives logout and rebootsbrew services start herdrbrew services list | grep herdr # should read "started"
# Or foreground, when you want to watch it or are debugging startupherdr serveConfig lives at ~/.config/herdr/config.toml. A reasonable starting point, matching the tmux prefix from step 6:
onboarding = false
[ui]sidebar_width = 26sidebar_min_width = 26sidebar_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.
9. Beads
Section titled “9. Beads”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:
# already done on this Mac via step 2brew install beads
# the VPS/Linux equivalent, for referencecurl -sSL https://raw.githubusercontent.com/gastownhall/beads/main/scripts/install.sh | bashThen, inside a repo that uses beads:
bd --version # confirm it matches every other machine on this projectcd ~/<repo>bd bootstrap --yes # clones the shared Dolt database from the git remotebd stats # sanity check: counts should match your other machinesgit config beads.role maintainer # match the role used on other dev machines for this repo10. Optional: Tailscale
Section titled “10. Optional: Tailscale”Needed only if this Mac has to reach VPS boxes whose SSH is locked to the tailnet.
brew install --cask tailscale# or, if you prefer the sandboxed App Store build (needs the `mas` CLI):mas install 1475387142Launch 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:
sudo ln -sf /Applications/Tailscale.app/Contents/MacOS/Tailscale /opt/homebrew/bin/tailscaletailscale statusFor 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.
Appendix: optional extras
Section titled “Appendix: optional extras”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
brew install coreutils bash stow watch pwgen macos-trash mas blueutilcoreutils 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
brew install actionlint shfmt ast-grep zizmoractionlint 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
brew install awscli aws-sam-cli ansible go openjdkDatabases and local services
brew install postgresql@14 mysql supabase temporalEach 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
brew install ffmpeg poppler pngpaste gallery-dlpoppler supplies pdftotext and pdfimages. pngpaste writes the clipboard image to a file, which is handy for feeding screenshots to an agent.
iOS and mobile
brew install cocoapods xcodegen swiftlint ideviceinstallerNetworking
brew install nmapLanguage extras
brew install pipx node@22 nvmpipx 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
brew install opencodeDeliberately 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).
LANGfirst, 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.uvcovers install, venv, run and one-off tools. - Homebrew services mean launchd. Anything under
brew servicesinherits a stripped environment. When a background service behaves differently from the same command in your terminal, compareenvoutput 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 --caskaway 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.