Case Study / iMac Pro

Stand up an agent on the iMac 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 iMac 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 before continuing. Three answers in there change the rest of the walkthrough: the macOS version, whether the machine is Intel or Apple Silicon, and whether it already has a toolchain.

Read the macOS line first. It is a hard gate, not a preference.

If the machine is below 13, stop and upgrade macOS before anything else. Ask what the machine actually does for its owner first, then check what Apple will serve it:

df -h /                        # want ~30GB free
softwareupdate --list-full-installers

Take the newest version that list offers and fetch it by exact version number:

softwareupdate --fetch-full-installer --full-installer-version 15.7.9
open "/Applications/Install macOS Sequoia.app"

Jumping several major versions at once is supported, no stepping stones. Budget one to two hours, nearly all of it download. Afterward run xcode-select --install and then brew update before anything else, because a major macOS upgrade leaves the command line tools stale and Homebrew then fails with a confusing SDK error that looks like Homebrew's own fault.

Caught live on the first machine this doc was used on: an iMac Pro on 12.7.6, which failed both conditions. The preflight surfaced it before anything had been installed, which is the entire reason Stage 0 exists.

01Base toolchain

Skip anything the preflight already found.

Resume here after a macOS upgrade

This machine came up on Sequoia on 2026-08-16. Run these in order and stop at the first one that errors, because a fresh major version leaves the command line tools stale and Homebrew's error for that looks like a Homebrew problem instead of an SDK one.

sw_vers                       # confirm 15.x
xcode-select --install        # GUI dialog; "already installed" is fine
uname -m                      # x86_64 on this machine, it is Intel

Because it is Intel, Homebrew lives at /usr/local, not /opt/homebrew, and the Apple Silicon node trap further down does not apply here.

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 iMac Pro, x86_64 node is correct and there is nothing to fix.

02Claude Code, installed and signed in

Use the native installer. It needs no Node, no Homebrew, and no sudo, so this stage can run before Stage 01 has finished downloading.

curl -fsSL https://claude.ai/install.sh | bash
export PATH="$HOME/.local/bin:$PATH"
claude --version

The binary lands at ~/.local/bin/claude, owned by the logged-in user, and updates itself in the background from then on.

Do not install this with npm on a machine where Node came from the .pkg installer. Node's package owns /usr/local/lib/node_modules as root, so npm install -g @anthropic-ai/claude-code dies with EACCES: permission denied. The obvious escape, sudo npm install -g, is the one Anthropic's docs explicitly warn against: it leaves root-owned files that break the auto-updater later. The native installer above sidesteps both.

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 iMac 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.

Decided 2026-08-16: this machine goes unattended from day one. An attended agent cannot answer the phone, and the phone is the entire point of the build, so the permission prompt would only ever fire into an empty room. The commands below are written for that. If you are following this page on a different machine and want the cautious version, drop --dangerously-skip-permissions from Stages 04 and 05 and skip the launchd job entirely, because a monitor that restarts an agent nobody is there to approve does nothing.

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 Case"

--remote-control Case 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 iMac 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
}

# A cold start stops at two full-screen prompts: the folder trust gate and the
# bypass-permissions warning. Nobody is here to answer them, so answer them.
accept_prompts() {
  for i in $(seq 1 20); do
    sleep 3
    pane=$(tmux capture-pane -t "$SESSION" -p -S -60 2>/dev/null)
    if [[ "$pane" == *"bypass permissions on"* ]]; then
      log "session ready"; return 0
    elif [[ "$pane" == *"Bypass Permissions mode"* ]]; then
      tmux send-keys -t "$SESSION" "2" Enter; log "accepted bypass warning"
    elif [[ "$pane" == *"project you created or one you trust"* ]]; then
      tmux send-keys -t "$SESSION" "1" Enter; log "accepted trust prompt"
    fi
  done
  log "WARNING: prompts not cleared after 60s"
}

# A live process is not the same as a usable agent. If the ready footer is
# missing -- prompt copy changed upstream, session wedged -- the process is
# still alive and healthy-looking, so check for readiness separately.
notready=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 Case"
    accept_prompts
    notready=0
  else
    pane=$(tmux capture-pane -t "$SESSION" -p -S -60 2>/dev/null)
    if [[ "$pane" == *"bypass permissions on"* ]]; then
      notready=0
    else
      notready=$((notready + 1))
      log "WARNING: session alive but not ready (${notready})"
      [ "$notready" -eq 4 ] && accept_prompts
      if [ "$notready" -ge 8 ]; then
        log "ERROR: never became ready, restarting the session"
        tmux kill-session -t "$SESSION" 2>/dev/null
        notready=0
      fi
    fi
  fi
  sleep 30
done
EOF
chmod +x ~/bin/claude-agent.sh

Why accept_prompts exists. Claude Code opens a cold session with two full-screen gates: "is this a folder you trust," and the bypass-permissions warning, whose default is No, exit. A monitor that only starts the process leaves it parked on those prompts forever, and everything above it looks healthy: launchd says running, tmux says the session exists, the pane pid is a real claude. The phone just never sees the machine. Caught on the first real reboot, 2026-08-16.

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>$HOME/.local/bin:$(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 or claude and the job fails silently while looking loaded. $HOME/.local/bin has to be in there, because that is where the native installer from Stage 02 puts claude and nothing else on the default path points at it. 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 iMac Pro is in the list.

Do this one

The Claude app on his iPhone

The --remote-control Case 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 iMac 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 Case. 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 iMac 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.