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
launchdstarts at login and restarts if they exit.
Both start when your user logs in. That’s why the FileVault / auto-login decision matters.
Login Items for apps
Section titled “Login Items for apps”System Settings → General → Login Items & Extensions
- Under Open at Login, click + and add each app you want running: for example Cursor, Claude, Tailscale, Google Drive and OneDrive.
- 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:
~/bin/health-check.sh --app Cursor --app ClaudeLaunchAgents for headless jobs
Section titled “LaunchAgents for headless jobs”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 or LaunchDaemon?
Section titled “LaunchAgent or LaunchDaemon?”| 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.
Anatomy of a LaunchAgent
Section titled “Anatomy of a LaunchAgent”The template keeps a Cursor self-hosted worker running:
<?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. |
Load, restart, inspect, unload
Section titled “Load, restart, inspect, unload”Modern launchctl uses domains: gui/<your user id> is your login session.
PLIST=~/Library/LaunchAgents/com.example.ai-worker.plistplutil -lint "$PLIST" # check the syntaxlaunchctl 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 codelaunchctl kickstart -k gui/$(id -u)/com.example.ai-worker # restart it nowlaunchctl bootout gui/$(id -u)/com.example.ai-worker # stop and unloadTo 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.
Or let the script do it
Section titled “Or let the script do it”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.
# 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-colorYour 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.
Privacy permissions for background jobs
Section titled “Privacy permissions for background jobs”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.
Check it
Section titled “Check it”~/bin/health-check.sh --agent com.example.cursor-workertail -f ~/Library/Logs/com.example.cursor-worker/stderr.logThe 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.