Case Study / Mac Pro

Stand up an agent on the Mac Pro

A persistent Claude Code agent living in tmux on Will's machine, surviving reboots, reachable from his phone. Staged so you can stop cleanly at any point and still have something that works.

2026-08-16 · run the stages in order · text Jared's agent the output of Stage 0 before going further

Before you start

00Preflight

Open Terminal on the Mac Pro. Paste this whole block and hit return. It changes nothing, it only reports.

echo "=== macOS ==="; sw_vers
echo "=== arch ==="; uname -m
echo "=== hardware ==="; system_profiler SPHardwareDataType | grep -E "Model Name|Model Identifier|Chip|Processor Name|Total Number of Cores|Memory"
echo "=== toolchain ==="; for t in brew node npm tmux git claude; do printf "%-8s " "$t"; command -v $t || echo "MISSING"; done
echo "=== node arch ==="; command -v node >/dev/null && file "$(command -v node)" || echo "no node"
echo "=== sleep ==="; pmset -g | grep -E "^ *(sleep|disksleep|hibernatemode|womp)"
echo "=== messages ==="; ls -la ~/Library/Messages/chat.db 2>/dev/null || echo "no chat.db, Messages not signed in"
echo "=== remote login ==="; sudo systemsetup -getremotelogin 2>/dev/null || echo "needs admin"

Send that output to Jared's agent over iMessage before continuing. Two answers in there change the rest of the walkthrough: whether the machine is Intel or Apple Silicon, and whether it already has a toolchain.

01Base toolchain

Skip anything the preflight already found.

Xcode command line tools

xcode-select --install

A GUI dialog appears. Click install, wait it out. If it says already installed, move on.

Homebrew

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

At the end it prints two lines to run to put brew on the PATH. Actually run them. The install does not do it for you and every later step fails without it. On Apple Silicon they reference /opt/homebrew, on Intel /usr/local. If you lose them:

# Apple Silicon
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"

# Intel
echo 'eval "$(/usr/local/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/usr/local/bin/brew shellenv)"

tmux, node, git

brew install tmux node git
node --version && tmux -V

Apple Silicon trap, check this before installing Claude. Run file $(command -v node). If it says x86_64 on an arm64 machine, stop. Claude Code's native binary needs real arm64, Rosetta will not carry it, and the install fails with "claude native binary not installed." Fix is to get arm64 node on the PATH ahead of the x64 one, easiest via nvm:

curl -fsSL -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"
nvm install --lts && nvm use --lts
file "$(command -v node)"   # must say arm64

On an Intel Mac Pro, x86_64 node is correct and there is nothing to fix.

02Claude Code, installed and signed in

npm install -g @anthropic-ai/claude-code
claude --version

Then start it once by hand, in a normal Terminal window, so the login happens with a human present:

cd ~
claude

It opens a browser to authorize. Sign in as Will, not Jared. Once you get a prompt, type /status to confirm the account and plan, then /exit.

If the browser flow will not complete on the Mac Pro, /login also prints a URL you can open on a phone and paste the code back.

03Decide the permission posture

This is the one real decision in the whole setup, so make it deliberately rather than copying Jared's.

ModeWhat happensRight when
claudeAsks before each new kind of actionWill is sitting at the machine, learning what it does
claude --dangerously-skip-permissionsNever asks. Required for an unattended agent, because there is nobody there to click yesThe agent has to run while nobody is watching

Jared's runs with permissions skipped because it takes work from his phone all day. That is a real tradeoff, not a formality: an unattended agent can delete files, spend money through connected accounts, and send messages as whoever it is signed in as. On a business machine that also runs the shop, start attended for the first week. Let Will watch what it actually does, then loosen it.

The rest of this page assumes he goes unattended eventually. Swap the flag out of the commands if he does not.

04The tmux session

tmux keeps the agent alive after you close Terminal or disconnect. Start it:

tmux new-session -d -s claude -c "$HOME" \
  "claude --dangerously-skip-permissions --remote-control CaseStudy"

--remote-control CaseStudy is the flag that makes Stage 07 work. It registers this session so the Claude app on Will's phone can reach it by name. Execution still happens entirely on the Mac Pro. Put it in now so you do not have to restart the session later.

Everyday commands:

tmux attach -t claude     # look at it / talk to it
# press ctrl-b then d      to leave it running
tmux ls                   # what sessions exist
tmux kill-session -t claude

The one thing people get wrong: closing the Terminal window is fine, pressing ctrl-c inside the session is not. Ctrl-b then d is the detach.

05Keep it alive across crashes and reboots

Two files. First the monitor loop:

mkdir -p ~/bin ~/Library/Logs
cat > ~/bin/claude-agent.sh <<'EOF'
#!/bin/bash
SESSION="claude"
WORKDIR="$HOME"
LOG="$HOME/Library/Logs/claude-agent.log"
log(){ echo "$(date '+%F %T') $*" >> "$LOG"; }

session_healthy() {
  tmux has-session -t "$SESSION" 2>/dev/null || return 1
  pane_pid=$(tmux list-panes -t "$SESSION" -F '#{pane_pid}' 2>/dev/null | head -1)
  [ -n "$pane_pid" ] || return 1
  ps -p "$pane_pid" -o args= 2>/dev/null | grep -q 'claude' || return 1
  return 0
}

while true; do
  if ! session_healthy; then
    log "session down, starting"
    tmux kill-session -t "$SESSION" 2>/dev/null
    tmux new-session -d -s "$SESSION" -c "$WORKDIR" \
      "claude --dangerously-skip-permissions --remote-control CaseStudy"
  fi
  sleep 30
done
EOF
chmod +x ~/bin/claude-agent.sh

Then the launchd job that runs it at login and restarts it if it dies:

cat > ~/Library/LaunchAgents/co.casestudy.claude-agent.plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>co.casestudy.claude-agent</string>
  <key>ProgramArguments</key>
  <array><string>$HOME/bin/claude-agent.sh</string></array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>EnvironmentVariables</key>
  <dict>
    <key>PATH</key><string>$(dirname "$(command -v brew)"):$HOME/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
    <key>HOME</key><string>$HOME</string>
  </dict>
  <key>StandardOutPath</key><string>$HOME/Library/Logs/claude-agent.out</string>
  <key>StandardErrorPath</key><string>$HOME/Library/Logs/claude-agent.err</string>
</dict>
</plist>
EOF
launchctl unload ~/Library/LaunchAgents/co.casestudy.claude-agent.plist 2>/dev/null
launchctl load ~/Library/LaunchAgents/co.casestudy.claude-agent.plist

The gotcha that has bitten this exact setup before. launchd hands a job a bare environment, not your login shell's. If PATH is not spelled out in the plist, the script cannot find tmux, node, or claude and the job fails silently while looking loaded. The EnvironmentVariables block above is not optional. Verify it actually took:

sleep 40; tmux ls; tail -5 ~/Library/Logs/claude-agent.log

Stop the machine sleeping

sudo pmset -a sleep 0 disksleep 0 womp 1
pmset -g | grep -E "^ *(sleep|disksleep|womp)"

Display sleep is fine and saves the panel. Machine sleep is what kills the agent. womp 1 lets it wake on network.

06Give it a brain

An agent with no context is a chatbot. Two files make it Will's:

mkdir -p ~/.claude
cat > ~/CLAUDE.md <<'EOF'
# Context

William Douglas. Case Study Coffee Lounge, 4802 N 16th St, Phoenix.
Also runs William Douglas Co (woodworking, visual art, consulting) and a
clothing line under the Case Study name.

## How to work with me
- Blunt and useful. Skip the preamble.
- Act, then report what you did. Do not ask permission for small things.
- If something will not work, say so and why.
- Name the tradeoff when there is one.

## Standing context
- Wood allergy means no sawdust-side production work. Operations, curation,
  design direction, and content instead.
- The clothing line is deliberately separate from the cafe brand identity.
EOF

Have Will rewrite that in his own words on day one. The file is loaded into every conversation, so it is the highest-leverage thing on the machine. Jared's is about 200 lines and took months to earn.

07Reach it from his phone

No terminal on the phone. No SSH client, no VPN app, no typing tmux commands with his thumbs. He opens the Claude app and his Mac Pro is in the list.

Do this one

The Claude app on his iPhone

The --remote-control CaseStudy flag from Stage 04 is the whole mechanism. It registers the running tmux session with Anthropic so the phone can find it. The work still runs on the Mac Pro, using Will's files, his shop network, his printer. The phone is a window, not the machine.

  1. Install the Claude app from the App Store on his iPhone.
  2. Sign in as the same account he authorized in Stage 02. This has to match or the session will not show up.
  3. The session appears as CaseStudy. Tap it and talk.

Nothing to install on the Mac beyond what Stage 04 already did. This works from anywhere, on cell, without touching Case Study's network settings.

Verify it from the Mac before you leave: the session prints a claude.ai/code URL when Remote Control connects. If you never see that line, the flag did not take and the phone will show nothing.

Later, if he wants to just text it

Jared's box also answers iMessage, through a relay that watches the Messages database, drops inbound texts into the tmux pane, and sends replies back out. It is roughly a thousand lines of custom code and it is a separate build, not a today job. Worth knowing that the two channels stack: iMessage and the Claude app both feed the same single agent, they do not compete.

Two things to settle before that gets built:


What good looks like when you leave

tmux ls                                  # claude: 1 windows ...
tail -3 ~/Library/Logs/claude-agent.log  # a start line, no error loop
pmset -g | grep " sleep"                 # sleep 0
claude --version                         # a version number

Then reboot the Mac Pro and check tmux ls again after it comes back up. If the session is there without anyone touching it, the setup is real.

Not doing today, on purpose

The memory system, the hooks, the cron jobs, the daily brief, MCP servers for his calendar and email. All of that is worth having and none of it matters until the base agent has been alive for a week and Will knows what he actually wants from it. Jared's box accumulated that over months, one problem at a time, and copying the end state would hand Will a machine he cannot debug.