Case Study / 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.
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.
Skip anything the preflight already found.
xcode-select --install
A GUI dialog appears. Click install, wait it out. If it says already installed, move on.
/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)"
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.
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.
This is the one real decision in the whole setup, so make it deliberately rather than copying Jared's.
| Mode | What happens | Right when |
|---|---|---|
claude | Asks before each new kind of action | Will is sitting at the machine, learning what it does |
claude --dangerously-skip-permissions | Never asks. Required for an unattended agent, because there is nobody there to click yes | The 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.
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
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
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.
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.
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.
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.
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.
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:
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.