Skip to main content

macos

Legacy v1/launchd documentation. This page maps the original systemd-style supervised agent workflow to macOS launchd. It does not describe the current containerized Go deployment (branch v4; code under src/). For that, start with src/docs/README.md, plus src/docs/operator-reference.md and src/docs/deployment-scripts.md.

The hive runtime was built on Linux/systemd, but the same concepts map cleanly to macOS using launchd β€” Apple's equivalent of systemd.

This guide shows how to run a supervised agent on a Mac that stays on (Mac Mini, Mac Studio, always-on laptop, etc.).


Concept mapping​

Linux (systemd)macOS (launchd)Role
hive.serviceLaunchAgent plistKeeps the supervisor alive, restarts on crash
hive-healthcheck.timerSecond LaunchAgent plist with StartCalendarIntervalPeriodic healthcheck
hive-renew.timerThird plist with 6-day interval/loop cron renewal
systemctl enable --nowlaunchctl load -wStart + enable at login
systemctl stoplaunchctl unloadStop
journalctl -u hiveStandardOutPath / StandardErrorPath in plistLogs
/etc/hive/agent.envPlist EnvironmentVariables dict or a sourced env fileConfig

Quickstart​

1. Install prerequisites​

brew install tmux curl jq
# Install your AI CLI (e.g., Claude Code)
npm install -g @anthropic-ai/claude-code
claude /login

2. Create the config directory​

mkdir -p ~/.config/hive
cp config/agent.env.example ~/.config/hive/agent.env
# Edit to match your setup:
nano ~/.config/hive/agent.env

3. Install the LaunchAgent​

Copy the example plist, adjust paths, and load it:

# Copy the template
cp launchd/com.hive.plist.example ~/Library/LaunchAgents/com.hive.plist

# Edit: change all /Users/YOURUSER paths to your actual home directory
nano ~/Library/LaunchAgents/com.hive.plist

# Create log directory
mkdir -p ~/.local/state/hive

# Load (starts immediately + starts on every login)
launchctl load -w ~/Library/LaunchAgents/com.hive.plist

4. Verify it's running​

# Check launchd status
launchctl list | grep hive

# Attach to the tmux session
tmux attach -t hive
# Detach: Ctrl+B then D

5. Uninstall​

launchctl unload ~/Library/LaunchAgents/com.hive.plist
rm ~/Library/LaunchAgents/com.hive.plist
# Optionally remove config + state:
# rm -rf ~/.config/hive ~/.local/state/hive

Healthcheck on macOS​

On Linux, the healthcheck is a separate systemd timer. On macOS, use a second LaunchAgent with StartCalendarInterval:

<!-- ~/Library/LaunchAgents/com.hive.healthcheck.plist -->
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.hive.healthcheck</string>
<key>ProgramArguments</key>
<array>
<string>/path/to/hive/bin/agent-healthcheck.sh</string>
</array>
<key>StartCalendarInterval</key>
<array>
<!-- Every 20 minutes: :00, :20, :40 -->
<dict><key>Minute</key><integer>0</integer></dict>
<dict><key>Minute</key><integer>20</integer></dict>
<dict><key>Minute</key><integer>40</integer></dict>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>AGENT_LOG_FILE</key>
<string>/Users/YOURUSER/.local/state/hive/heartbeat.log</string>
<key>AGENT_SESSION_NAME</key>
<string>hive</string>
<key>AGENT_STALE_MAX_SEC</key>
<string>1800</string>
<key>AGENT_MAX_RESPAWNS</key>
<string>3</string>
<key>NTFY_TOPIC</key>
<string>your-secret-topic</string>
</dict>
<key>StandardOutPath</key>
<string>/Users/YOURUSER/.local/state/hive/healthcheck.log</string>
<key>StandardErrorPath</key>
<string>/Users/YOURUSER/.local/state/hive/healthcheck.err</string>
</dict>
</plist>

Renew timer on macOS​

The renew timer kills and respawns the tmux session every 6 days to beat Claude Code's 7-day /loop expiry. On macOS, this is harder to express as a calendar interval (launchd doesn't have "every N days" natively).

Recommended approach: use a wrapper script that checks the session age:

#!/bin/bash
# renew-if-stale.sh β€” run hourly via launchd, only acts every 6 days
SESSION="${AGENT_SESSION_NAME:-hive}"
STATE_DIR="${HOME}/.local/state/hive"
STAMP="$STATE_DIR/last-renew"

# If stamp doesn't exist, create it and exit
[ -f "$STAMP" ] || { date +%s > "$STAMP"; exit 0; }

AGE=$(( $(date +%s) - $(cat "$STAMP") ))
if [ "$AGE" -ge 518400 ]; then # 6 days in seconds
tmux kill-session -t "$SESSION" 2>/dev/null
date +%s > "$STAMP"
# Supervisor will detect the missing session and respawn
fi

Then set a launchd plist with StartInterval of 3600 (hourly check).


Differences from Linux​

AreaLinuxmacOS
Shell/bin/bash everywhere/bin/zsh default; use /opt/homebrew/bin/bash for bash 5+ features (associative arrays)
stat flagsstat -c %Y filestat -f %m file
Process managementsystemctl start/stop/restartlaunchctl load/unload
Auto-startsystemctl enableload -w flag persists across reboots
Log viewingjournalctl -u name -ftail -f /path/to/log (or Console.app)
File lockingflock (coreutils)flock via brew install util-linux or use lockfile pattern
date commandGNU date (date -d)BSD date (no -d; use date -j -f)

Alternative scheduler: standalone scanner script​

On macOS, some deployments skip the full supervisor+tmux pattern entirely and use a standalone scanner script fired by launchd on a fixed schedule. The script does the scanning/state-tracking work in bash, then triggers the AI agent (via a Copilot CLI skill, tmux work order, or similar) only when there's actionable work.

This pattern is simpler when:

  • The scanning logic is deterministic (no LLM needed for triage)
  • You want the scanner to run even when the AI session is down
  • You want to decouple scan cadence from agent availability

See examples/worker.sh.example for a reference implementation and examples/kubestellar-fixer.md for a full case study of this pattern in production.