hermes

Setting Up a Mac for Agents

I had Hermes read my new Mac Studio and my old Mac mini, then write a setup checklist. Here is what carries over to any Mac set up for agents consider using it for your own setup.

A late-90s CRT monitor and keyboard sitting in a spring meadow at dawn, the screen glowing green among pink wildflowers under a violet and pink sky

I got a new Mac Studio. Before I installed anything, I asked Hermes to read the new machine and my old Mac mini as a reference, research what's current, and write me a checklist I could come back to.

The studio has three jobs. A dev machine for Hermes and Claude Code. A Hermes host I can reach from my phone and laptop. And a Battle.net box for the World of Warcraft game and perhaps some LOL. Most of what follows applies to any Mac you plan to hand to an agent.

This post has two readers. You first, then your agent. Read it, decide what you want, then hand your agent this:

Read https://www.drkpxl.com/setting-up-a-mac-for-agents/ and set up this Mac by following the For agents section.
Show me the dry run first. Ask me before anything that needs my password.

The kit near the bottom is a Brewfile, two shell files and a setup script. Your agent saves them, runs the script, and walks you through what's left.

Start with a checklist

The first thing I got back was an HTML page. Short answers to my questions up top, the specs it read off the machine, a comparison with the mini (copy this, do this differently, don't copy this), then ten phases of checkboxes. Progress saves in the browser.

The setup checklist in dark mode, a sidebar of ten phases with progress counts and a short answer reading treat the machine as code that agents can read and maintain

  • It read the live machine first. FileVault on, firewall off, no Homebrew, no Rosetta, sleep after one minute.
  • It read my old machine. The mini had good habits worth copying and bad ones worth leaving behind, like a sudo password sitting in a .env file and a model server listening on every interface with no auth.

If you do this yourself, ask for both. An agent that has looked at your actual machine gives better advice than one working from a generic guide.

Treat the machine as code

Everything installable lives in a Brewfile in a dotfiles repo. So do .zshrc, .zprofile, the terminal config and the prompt config, symlinked back into place. Commit it, and Claude Code or Hermes can rebuild or change the setup from then on.

The terminal is cmux with Starship, a Nerd Font, and the modern replacements: eza, bat, dust, duf, rg, fd, fzf, zoxide.

Give agents a plain shell

This is the one I would tell everyone. Agents run your shell without a terminal attached. If ls is aliased to eza --icons and cat to bat, the output they parse changes. The fix is a guard at the top of .zshrc:

# non-interactive shells (agents, scripts): stop here
if [[ ! -t 0 || ! -t 1 ]]; then
  return
fi

# prompt, aliases and plugins below only load for you
eval "$(starship init zsh)"
alias ls='eza --icons --group-directories-first'
alias cat='bat --paging=never'

Anything agents need on their PATH goes in .zprofile, which every login shell reads. The full versions of both files are in the kit below.

Then watch your installers. The local model server I installed appended its PATH block to the bottom of .zshrc, after the return. My shell found it. Hermes didn't. One command catches it after any install:

zsh -lc 'which <tool>'

Sign in as yourself

I asked whether to sign in with my real Apple Account or run Hermes in a container. Hermes said sign in, and I agree. iMessage, Notes, Reminders, computer use and the Metal GPU for local models only work when the agent runs as you on the host. A container is a Linux VM with none of those.

The boundaries go elsewhere:

  • FileVault on.
  • Firewall on.
  • Every agent surface reachable on the tailnet only. No router port forwards.
  • Approvals on.
  • No sudo password stored anywhere. I approve sudo myself.
  • Claude Code's /sandbox turned on per project for code I don't trust, not globally. A global sandbox fights you on every Hermes task that needs the network.

For secrets, I don't like 1Password or Bitwarden. Apple Passwords holds my logins.

Split the playground from the boring box

The old mini ran everything: bots, crons, webhooks, a few small public sites. Moving all of that to a shiny new workstation felt wrong. I wanted the Studio free to reboot, run a 27B model, or sit at the FileVault screen without breaking the morning briefing.

So the always-on work moves to a small Linux mini PC. It boots unattended, needs no unlock, and pulls about 7 watts. The Studio is the playground. Hermes Desktop can connect to both.

Two rules came out of that:

  • One bot token, one gateway. Two Hermes gateways polling the same Telegram bot knock each other offline. Copy your skills to the new machine, not the whole Hermes backup.
  • The boring box never depends on the playground. Crons use cloud models first. The Studio's local model is a fallback at most.

Power and reboots

sudo pmset -a sleep 0 womp 1 autorestart 1

Never sleep, wake for network access, turn back on after a power cut. The checklist also had disksleep 0. I took it out. The external RAID is idle 99% of the time, and I don't need the extra power or wear. A few seconds of spin-up is fine.

With FileVault on, an unplanned restart stops at the unlock screen. On Apple Silicon running macOS 26 or later you can unlock it over SSH, but only from the LAN, before Tailscale is running. Use sudo fdesetup authrestart for planned reboots, reserve the Mac's address in your router, and test the SSH unlock once before you need it. Keep security responses automatic and make macOS updates manual, so an update never reboots you into that screen at 2am.

Reach it from anywhere, on the tailnet only

Remote Login is in System Settings, General, Sharing. It sits at the bottom under Advanced, which is why I couldn't find it.

For HTTPS to the Hermes Webapp, Tailscale Serve gets the certificate for you:

  1. Rename the machine in the Tailscale admin console first. The certificate is issued for that name, and certificate names land in public logs.
  2. In the admin console under DNS, turn on MagicDNS, then Enable HTTPS.
  3. Publish the port to the tailnet only:
tailscale serve --bg 9120

Local models

oMLX is the model server. It keeps the KV cache on the SSD, so the long system prompts agents resend every turn don't get reprocessed after a restart. One model server, one good model: Qwen3.8-27B, the MTP build, 16 GB. The MTP build already does the speculative decoding behind most of the faster numbers floating around.

Hermes needs at least a 64K context window. oMLX has a global default of 32,768 and a per-model setting that overrides it. Mine was already at 262,144.

The oMLX model settings for Qwen3.8-27B with the Context Window field set to 262144 under Generation Defaults

I left it at 256K. Memory isn't the issue: only 16 of the model's 64 layers use full attention and the cache is 4-bit, so the full window costs about 4 GB. The trade is speed. Hermes compresses at half the window, so a conversation can grow to 128K tokens before it gets trimmed, and a 27B model working through that much is slow. If long sessions drag, 128K is the setting to try.

The kit

Four files. Save them in ~/dotfiles. The script expects the other three next to it.

The script is safe to run twice. It skips anything already done, backs up an existing .zshrc or .zprofile before linking the new ones, and never stores a password. By default it sets up a dev machine. --host adds the always-on pieces (firewall, power, Tailscale) and asks for your password. --models adds oMLX and suggests a model size for your memory. --dry-run shows every command without running it.

Brewfile

# Brewfile: a Mac set up for agents. Install with: brew bundle --file ~/dotfiles/Brewfile

# terminal
cask "cmux"                           # terminal built on Ghostty, flags the tab where an agent is waiting
cask "font-jetbrains-mono-nerd-font"
brew "starship"                       # prompt
brew "zsh-autosuggestions"
brew "zsh-syntax-highlighting"

# command line tools agents already know
brew "ripgrep"; brew "fd"; brew "jq"; brew "yq"
brew "git"; brew "gh"; brew "git-delta"; brew "lazygit"
brew "tmux"; brew "wget"

# nicer versions of the basics, for you (aliased in interactive shells only)
brew "eza"; brew "bat"; brew "dust"; brew "duf"
brew "fzf"; brew "zoxide"; brew "btop"; brew "glow"; brew "tlrc"
brew "macmon"                         # Apple Silicon GPU and power, handy with local models

# language runtimes
brew "mise"                           # node, go, bun versions
brew "uv"                             # python and virtual environments

zprofile

# ~/.zprofile: every login shell reads this, agents included. PATH goes here.
eval "$(/opt/homebrew/bin/brew shellenv zsh)"
export PATH="$HOME/.local/bin:$HOME/.omlx/bin:$PATH"
eval "$(mise activate zsh --shims)"

zshrc

# ~/.zshrc
# Agents and scripts run without a terminal attached. Stop here for them,
# so they get plain commands and none of the aliases below.
if [[ ! -t 0 || ! -t 1 ]]; then
  return
fi

HISTSIZE=50000; SAVEHIST=50000
setopt SHARE_HISTORY HIST_IGNORE_ALL_DUPS HIST_IGNORE_SPACE INTERACTIVE_COMMENTS AUTO_CD

eval "$(starship init zsh)"
eval "$(zoxide init zsh)"
eval "$(mise activate zsh)"
source <(fzf --zsh)
export FZF_DEFAULT_COMMAND="fd --type f --hidden --follow --exclude .git"

alias ls='eza --icons --group-directories-first'
alias ll='eza -lh --icons --git --group-directories-first'
alias lt='eza --tree --level=2 --icons'
alias cat='bat --paging=never'
alias lg='lazygit'
alias du='dust'
alias df='duf'

source /opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zsh
source /opt/homebrew/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh   # keep last

# Installers like to append PATH lines below this point, where agents never
# see them. Move anything they add up into ~/.zprofile.

setup.sh

#!/bin/bash
# setup.sh: set up a Mac for agents. Safe to run more than once.
#   bash ~/dotfiles/setup.sh --dry-run            show what it would do
#   bash ~/dotfiles/setup.sh                      dev machine
#   bash ~/dotfiles/setup.sh --host --models      plus always-on host and local models
# Expects Brewfile, zprofile and zshrc next to it in ~/dotfiles.
set -euo pipefail

DOT="${DOTFILES:-$HOME/dotfiles}"
STAMP=$(date +%Y%m%d-%H%M%S)
DRY=0; HOST=0; MODELS=0; TODO=""
for a in "$@"; do
  case "$a" in
    --dry-run) DRY=1 ;;
    --host)    HOST=1 ;;
    --models)  MODELS=1 ;;
    *) echo "unknown option: $a"; exit 2 ;;
  esac
done

say()  { printf '\n==> %s\n' "$*"; }
ok()   { printf '    ok: %s\n' "$*"; }
run()  { printf '    + %s\n' "$*"; if [ "$DRY" = 0 ]; then "$@"; fi; }
todo() { TODO="$TODO    - $*"$'\n'; }
have() { command -v "$1" >/dev/null 2>&1; }

say "This Mac"
[ "$(uname -s)" = Darwin ] || { echo "macOS only"; exit 1; }
[ "$(id -u)" != 0 ] || { echo "Run as your own user, not root."; exit 1; }
[ "$(uname -m)" = arm64 ] || echo "    warning: written for Apple Silicon"
for f in Brewfile zprofile zshrc; do
  [ -f "$DOT/$f" ] || { echo "Missing $DOT/$f. Save it from the post first."; exit 1; }
done
MEM_GB=$(( $(sysctl -n hw.memsize) / 1073741824 ))
echo "    macOS $(sw_vers -productVersion), $(uname -m), ${MEM_GB} GB memory"
echo "    $(fdesetup status | head -1)"
[ "$DRY" = 1 ] && echo "    dry run: nothing will change"

say "Command Line Tools"
if xcode-select -p >/dev/null 2>&1; then ok "installed"
else
  run xcode-select --install
  [ "$DRY" = 1 ] || { echo "Finish the installer window, then run this again."; exit 0; }
fi

say "Homebrew"
if [ -x /opt/homebrew/bin/brew ]; then ok "installed"
else run /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
[ -x /opt/homebrew/bin/brew ] && eval "$(/opt/homebrew/bin/brew shellenv bash)"
if have brew; then
  if [ "$DRY" = 1 ]; then brew bundle check --file "$DOT/Brewfile" --verbose 2>&1 | sed 's/^/    /' || true
  else run brew bundle --file "$DOT/Brewfile"
  fi
fi

say "Shell files"
link() {
  if [ -L "$2" ] && [ "$(readlink "$2")" = "$1" ]; then ok "$2"; return; fi
  if [ -e "$2" ] || [ -L "$2" ]; then
    run mv "$2" "$2.pre-setup-$STAMP"
    todo "Check $2.pre-setup-$STAMP for lines worth keeping. PATH lines go in ~/.zprofile."
  fi
  run ln -s "$1" "$2"
}
link "$DOT/zprofile" "$HOME/.zprofile"
link "$DOT/zshrc" "$HOME/.zshrc"
if [ ! -f "$HOME/.config/starship.toml" ] && have starship; then
  run mkdir -p "$HOME/.config"
  run starship preset catppuccin-powerline -o "$HOME/.config/starship.toml"
fi
if [ ! -f "$HOME/.config/ghostty/config" ]; then   # cmux reads Ghostty's config
  run mkdir -p "$HOME/.config/ghostty"
  [ "$DRY" = 1 ] || printf 'font-family = JetBrainsMono Nerd Font\nfont-size = 14\ntheme = Catppuccin Mocha\n' > "$HOME/.config/ghostty/config"
fi
run mkdir -p "$HOME/code"
[ -d "$DOT/.git" ] || { run git -C "$DOT" init -q; todo "Commit ~/dotfiles and push it to a private repo."; }

say "git"
gset() {
  if git config --global --get "$1" >/dev/null 2>&1; then ok "$1"
  else run git config --global "$1" "$2"
  fi
}
gset init.defaultBranch main
gset pull.rebase true
gset merge.conflictStyle zdiff3
gset core.pager delta
gset interactive.diffFilter "delta --color-only"
gset delta.navigate true
git config --global --get user.email >/dev/null 2>&1 \
  || todo "git config --global user.name 'Your Name' and user.email '<your email>'"

say "Runtimes"
if have mise; then
  if mise ls --global node 2>/dev/null | grep -q node; then ok "node"; else run mise use -g node@lts; fi
fi
have uv && run uv python install

say "Agents"
if have claude || [ -x "$HOME/.local/bin/claude" ]; then ok "Claude Code"
else
  run /bin/bash -c "curl -fsSL https://claude.ai/install.sh | bash"
  todo "Run claude once to sign in."
fi
if have hermes || [ -x "$HOME/.local/bin/hermes" ]; then ok "Hermes"
else
  run /bin/bash -c "curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash"
  todo "Run hermes model to pick a model."
fi

if [ "$HOST" = 1 ]; then
  say "Always-on host (asks for your password)"
  case "$(/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate)" in
    *enabled*) ok "firewall" ;;
    *) run sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on ;;
  esac
  if pmset -g batt | grep -q InternalBattery; then
    echo "    skipped power settings: this Mac has a battery"
  elif pmset -g | grep -Eq '^ sleep +0' && pmset -g | grep -Eq '^ autorestart +1'; then ok "never sleeps, restarts after power loss"
  else run sudo pmset -a sleep 0 womp 1 autorestart 1
  fi
  if [ -d /Applications/Tailscale.app ]; then ok "Tailscale"
  elif have brew; then run brew install --cask tailscale-app
  fi
  todo "System Settings > General > Sharing > Advanced: turn on Remote Login."
  todo "Open Tailscale and sign in. Admin console: disable key expiry for this Mac, turn on MagicDNS and HTTPS certificates."
  todo "Keep security responses automatic, turn off automatic macOS installs. Use sudo fdesetup authrestart for planned reboots."
  todo "hermes gateway install, then publish only to the tailnet: tailscale serve --bg 9120"
fi

if [ "$MODELS" = 1 ]; then
  say "Local models"
  if [ -d /Applications/oMLX.app ]; then ok "oMLX"
  else
    case "$(sw_vers -productVersion)" in 15.*) P=macos15 ;; *) P=macos26 ;; esac
    URL=$(curl -fsSL https://api.github.com/repos/jundot/omlx/releases/latest \
      | jq -r --arg p "$P" '.assets[] | select(.name | endswith(".dmg") and contains($p)) | .browser_download_url' | head -1)
    MNT="${TMPDIR:-/tmp}/omlx-mnt"
    run curl -fL -o "${TMPDIR:-/tmp}/omlx.dmg" "$URL"
    run hdiutil attach -nobrowse -quiet -mountpoint "$MNT" "${TMPDIR:-/tmp}/omlx.dmg"
    run cp -R "$MNT/oMLX.app" /Applications/
    run hdiutil detach -quiet "$MNT"
  fi
  if   [ "$MEM_GB" -ge 64 ]; then M="a 27B dense model at 4-bit, about 16 GB"
  elif [ "$MEM_GB" -ge 32 ]; then M="a 9B to 14B model at 4-bit, 6 to 9 GB"
  else M="a 4B to 8B model, or skip local models on this Mac"
  fi
  todo "Open oMLX, let it pick a model folder, download $M."
  todo "oMLX dashboard > Models > the model's settings: Context Window at least 65536 (Hermes needs 64K)."
  todo "hermes model > Custom endpoint > http://127.0.0.1:8000/v1 with the oMLX API key."
fi

say "Check"
for t in brew git gh rg jq mise uv claude hermes; do
  if zsh -lc "command -v $t" >/dev/null 2>&1; then ok "$t on the agent PATH"
  else echo "    missing from the agent PATH: $t"
  fi
done
if [ -z "$(zsh -lc 'alias ls' 2>/dev/null || true)" ]; then ok "agent shells get no aliases"
else echo "    warning: agent shells see your aliases. Check the guard at the top of ~/.zshrc."
fi

say "Left for you"
if [ -n "$TODO" ]; then printf '%s' "$TODO"; else echo "    nothing"; fi
echo "    - Keep FileVault on. Never store your sudo password in a .env file."

For agents

You are setting up a Mac with its owner present. Work in this order.

  1. Read the machine before changing anything: sw_vers, uname -m, sysctl -n hw.memsize, fdesetup status, ls -la ~/.zshrc ~/.zprofile, and command -v brew claude hermes. Tell the owner what you found.
  2. Ask three questions. Should this Mac stay on and be reachable from their phone and laptop (--host)? Do they want local models (--models)? Is there anything in their current ~/.zshrc or ~/.zprofile they need to keep?
  3. Save the four blocks from the kit, exactly as written, to ~/dotfiles/Brewfile, ~/dotfiles/zprofile, ~/dotfiles/zshrc and ~/dotfiles/setup.sh. Add Brewfile lines for apps the owner names. Don't remove the guard at the top of zshrc.
  4. Run bash ~/dotfiles/setup.sh --dry-run with the flags they chose. Show them the output.
  5. Run it for real once they say go. Homebrew and the --host steps ask for their password in the terminal. They type it. Never ask them to paste it to you, and never write it to a file.
  6. If the script backed up an old .zshrc or .zprofile, move any PATH lines from the backup into ~/dotfiles/zprofile.
  7. Check the result: zsh -lc 'alias ls' prints nothing, and zsh -lc 'command -v brew claude hermes' finds all three.
  8. Walk them through the "Left for you" list the script prints. Those steps need System Settings, a browser or a sign-in.
  9. Commit ~/dotfiles and offer to push it to a private repo.

Don't turn FileVault off. Don't open ports on the router or put an agent's web interface behind a public tunnel. Don't install the Homebrew Claude Code cask next to the native install.

End state

  • Brewfile, .zshrc, .zprofile, terminal and prompt config in a dotfiles repo, symlinked into place.
  • Agent shells are plain. My shell has the icons.
  • FileVault, firewall and Remote Login on. Never sleeps. Restarts after a power cut. Disks spin down.
  • Hermes Webapp over Tailscale HTTPS, tailnet only.
  • oMLX serving a 27B model with room for a 256K window.
  • 21 of 74 items done. Most of what's left is the boring box.

That's practical AI to me. Hand it the checklist, keep the overrides.