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 psmA 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 1Without 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
- launchd.info, the readable reference
- Apple, creating launch daemons and agents
- Claude Code setup and auto-updates