Skip to content

5. Login items & LaunchAgents

Apps
Login Items
Headless jobs
LaunchAgents
Script
install-launchagent.sh

There are two kinds of things to keep running:

  • Apps with a window or menu-bar icon — Cursor, Claude, Tailscale, Google Drive, OneDrive. These go in Login Items.
  • Headless commands — a Cursor self-hosted worker, a bot, a scheduled script. These become LaunchAgents, which macOS’s launchd starts at login and restarts if they exit.

Both start when your user logs in. That’s why the FileVault / auto-login decision matters.

System Settings → General → Login Items & Extensions

  1. Under Open at Login, click + and add each app you want running: for example Cursor, Claude, Tailscale, Google Drive and OneDrive.
  2. Under Allow in the Background, make sure the helpers for those apps are switched on. macOS shows a “Background Items Added” notification when an app registers one; if you dismissed it or switched it off, the app’s background service won’t run.

Check after a restart with:

Terminal window
~/bin/health-check.sh --app Cursor --app Claude

A LaunchAgent is a small XML property list (.plist) in ~/Library/LaunchAgents/ that tells launchd what to run, where, and what to do when it exits.

LaunchAgent LaunchDaemon
Lives in ~/Library/LaunchAgents/ /Library/LaunchDaemons/
Runs as You root (or a UserName you set)
Starts When you log in At boot, before anyone logs in
Can use your Keychain, GUI, signed-in CLIs Yes No
Use for AI tools, workers, anything using your logins System services that need no user session

AI tools almost always belong in a LaunchAgent: they need your login credentials, your home folder, and sometimes your screen. Remember that with FileVault on, even LaunchDaemons wait until the disk is unlocked.

The template keeps a Cursor self-hosted worker running:

~/Library/LaunchAgents/com.example.ai-worker.plist
<?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>com.example.ai-worker</string>
<key>ProgramArguments</key>
<array>
<string>/Users/YOUR_USER/.local/bin/agent</string>
<string>worker</string>
<string>start</string>
<string>--name</string>
<string>your-mac-mini</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/YOUR_USER/code/my-project</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>30</integer>
<key>StandardOutPath</key>
<string>/Users/YOUR_USER/Library/Logs/com.example.ai-worker/stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/YOUR_USER/Library/Logs/com.example.ai-worker/stderr.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/Users/YOUR_USER/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
</dict>
</plist>
Key Meaning
Label Unique name. Use reverse-DNS, and name the file <Label>.plist.
ProgramArguments The command, one <string> per argument. The first must be an absolute path.
WorkingDirectory Folder the command runs in.
RunAtLoad Start as soon as the agent is loaded (at login).
KeepAlive Restart whenever it exits. For periodic jobs, use StartInterval (seconds) instead.
ThrottleInterval Minimum seconds between restarts, so a crash loop can’t hog the CPU.
StandardOutPath / StandardErrorPath Log files. Create the folder first.
EnvironmentVariables launchd starts jobs with a minimal PATH; add Homebrew and ~/.local/bin.

Modern launchctl uses domains: gui/<your user id> is your login session.

Terminal window
PLIST=~/Library/LaunchAgents/com.example.ai-worker.plist
plutil -lint "$PLIST" # check the syntax
launchctl bootstrap gui/$(id -u) "$PLIST" # load (and start, thanks to RunAtLoad)
launchctl print gui/$(id -u)/com.example.ai-worker | head -n 30 # state, pid, last exit code
launchctl kickstart -k gui/$(id -u)/com.example.ai-worker # restart it now
launchctl bootout gui/$(id -u)/com.example.ai-worker # stop and unload

To change a plist, bootout, edit, then bootstrap again. You’ll still see the older launchctl load / unload in many tutorials; they work but are legacy — prefer bootstrap / bootout.

install-launchagent.sh generates the plist, validates it with plutil, creates the log folder, and loads it. It’s a dry run until you add --apply, and running it again with the same options does nothing.

Terminal window
# Keep a Cursor worker running 24/7 in a project folder
~/bin/install-launchagent.sh --label com.example.cursor-worker \
--workdir ~/code/my-project \
-- ~/.local/bin/agent worker start --name your-mac-mini
# Looks right? Install it:
~/bin/install-launchagent.sh --apply --label com.example.cursor-worker \
--workdir ~/code/my-project \
-- ~/.local/bin/agent worker start --name your-mac-mini
# Run the health check every hour, logging to ~/Library/Logs/com.example.health-check/
~/bin/install-launchagent.sh --apply --label com.example.health-check --interval 3600 \
-- ~/bin/health-check.sh --no-color

Your shell expands ~ before the script sees it, so the plist gets absolute paths. The script refuses a relative --workdir or --log-dir (such as .), because launchd wouldn’t resolve it.

macOS protects some folders — Desktop, Documents, Downloads, iCloud Drive and other cloud-storage folders, removable volumes — and asks the first time an app reads them. A background job can’t click “Allow”, so it just fails with Operation not permitted.

  • Best: keep the files your jobs use outside protected folders, for example in ~/code/.
  • If a job must read a protected folder, run it once by hand while logged in (from Terminal or via Screen Sharing) and approve the prompt, or grant the specific program Full Disk Access in System Settings → Privacy & Security. Avoid granting Full Disk Access to broad tools like /bin/bash.
  • Tools that control the screen (for example a Cursor worker started with --computer-use) also need Accessibility and Screen & System Audio Recording permission for their helper app.
Terminal window
~/bin/health-check.sh --agent com.example.cursor-worker
tail -f ~/Library/Logs/com.example.cursor-worker/stderr.log

The health check lists every plist in ~/Library/LaunchAgents/, says whether each one is loaded and running, and shows the last exit code (or, for a crash, the signal such as Killed: 9) for any that stopped.