launchd, the boring hero

Thirty lines of XML and a power setting are the difference between a script and a service.

You said getting the mini to come back on its own sounded like more trouble than it was worth. It is one file and one checkbox.

The file

A LaunchAgent is a plist in ~/Library/LaunchAgents/. Four keys matter. ProgramArguments is the command. RunAtLoad starts it at login. KeepAlive restarts it when it exits. EnvironmentVariables sets PATH, because launchd does not read your shell profile and claude and tmux are not on its default path. Logs go where StandardOutPath and StandardErrorPath point. The starter in Downloads is exactly that.

cp net.psm.claude-agent.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/net.psm.claude-agent.plist
launchctl list | grep psm

A LaunchAgent runs inside your login session, which is what Claude Code needs for the keychain. A LaunchDaemon runs before login as root and is the wrong tool here. If the mini asks for a password on boot, turn on automatic login for your user so the agent session exists without you.

The tmux wrapper

Claude Code is interactive, so the plist starts a tmux session and then waits on it. When the session dies, the wait ends, launchd sees the exit, and KeepAlive starts it again. You attach with tmux attach -t agent from any terminal, including over SSH from your laptop on Tailscale, and detach with Ctrl-b d without stopping anything.

The checkbox

A Mac does not power back on after an outage unless you tell it to. System Settings, Energy, “Start up automatically after a power failure,” or from a terminal:

sudo pmset -a autorestart 1

Without this the UPS buys you the short outages and nothing else. With it, a long outage ends with the mini booting, logging in, launchd loading, tmux starting, Claude reconnecting the channel, and your bot answering again, with nobody home.

Two traps

Claude Code’s native installer updates itself in the background and applies the update on the next launch, which for an always-on session means the next time launchd restarts it. That is fine until an update changes behaviour under you. If you want to control when that happens, install with Homebrew instead, which does not auto-update, or set DISABLE_AUTOUPDATER=1 in the plist’s environment.

Two installs on one Mac is the other one. Homebrew and the native installer both put a claude on your path, and claude update can report success while claude --version in your plist’s PATH says otherwise. which -a claude shows both. Keep one.

How you know it worked

launchctl list | grep psm shows a PID. tmux ls shows agent. Reboot, wait a minute, text the bot.

Read more