Load this when designing, debugging, or disabling a launchd service. Covers plist semantics, domain targets, the disable vs bootout vs unload distinction, and the Apple Silicon system extension story.
launchd is macOS's init system AND its services manager — PID 1. It replaces init, cron, at, xinetd, inetd, and various startup hooks. Everything that runs as a background process on macOS — Apple's daemons, third-party agents, helper tools — is started, monitored, and (when necessary) restarted by launchd.
A "launchd job" is described by a property list (plist). The plist names the job (Label), tells launchd what to run (ProgramArguments), when to run it (RunAtLoad, KeepAlive, StartCalendarInterval, WatchPaths), and how to handle failures (ThrottleInterval, ExitTimeOut).
| Path | Scope | Loaded as |
|---|---|---|
~/Library/LaunchAgents/*.plist |
Current user only | gui/$UID |
/Library/LaunchAgents/*.plist |
Any logged-in user | gui/$UID per user |
/Library/LaunchDaemons/*.plist |
System-wide, runs as specified UID (usually root) | system |
/System/Library/LaunchAgents/*.plist |
Apple's per-user agents | gui/$UID (read-only) |
/System/Library/LaunchDaemons/*.plist |
Apple's daemons | system (read-only) |
Agent vs Daemon:
The most common third-party startup item is a LaunchAgent in /Library/LaunchAgents/ — system-installed (admin needed to write there) but runs per-logged-in-user.
Essential keys:
| Key | Type | Purpose |
|---|---|---|
Label |
string | Unique identifier (reverse-DNS by convention, e.g. com.example.MyDaemon) |
ProgramArguments |
array | argv to exec — [interpreter, arg1, arg2...] |
Program |
string | (alternative) single binary path; rarely used now |
RunAtLoad |
bool | Run once immediately when the job is loaded |
KeepAlive |
bool or dict | Restart the process if it exits (see below for dict form) |
ThrottleInterval |
int | Minimum seconds between restarts (default 10) |
StartCalendarInterval |
dict | Cron-style schedule (Minute, Hour, Day, Weekday, Month) |
StartInterval |
int | Run every N seconds |
WatchPaths |
array | Run when any of these paths changes |
QueueDirectories |
array | Run when any of these dirs becomes non-empty |
StandardOutPath |
string | Redirect stdout to this file |
StandardErrorPath |
string | Redirect stderr to this file |
EnvironmentVariables |
dict | Env vars for the launched process |
UserName |
string | UID to run as (daemons only) |
GroupName |
string | GID to run as |
WorkingDirectory |
string | cwd |
Disabled |
bool | Initial disabled state (rarely used — prefer launchctl disable) |
LimitLoadToSessionType |
string | "Aqua" (logged-in user), "Background", "LoginWindow", "System" |
MachServices |
dict | Mach service names this process publishes |
Sockets |
dict | Sockets to set up before the program runs |
LaunchOnlyOnce |
bool | Once loaded, never re-run |
KeepAlive as a dict (more nuanced):
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key><false/> <!-- only restart on failure -->
<key>NetworkState</key><true/> <!-- only run when network is up -->
<key>PathState</key> <!-- only run while paths exist -->
<dict>
<key>/usr/local/bin/foo</key><true/>
</dict>
<key>Crashed</key><true/> <!-- only restart if crashed -->
</dict>
launchctl operations take a domain/label pair. The domain determines which launchd instance hosts the job.
| Domain | Form | What it covers |
|---|---|---|
system |
system |
Root-level daemons (/Library/LaunchDaemons/, /System/Library/LaunchDaemons/) |
user/<UID> |
user/501 |
A specific user's background tasks (no GUI) |
gui/<UID> |
gui/501 |
A specific user's GUI session (most LaunchAgents live here) |
pid/<PID> |
pid/12345 |
A single process's environment |
Most operations on user agents target gui/$UID because that's where Aqua-session agents run.
The three commands look interchangeable but aren't. Choose based on intent.
launchctl disable <domain>/<label>Effect: Marks the job as disabled. The mark persists across reboots. The job will not be loaded next time launchd starts.
Does NOT: Stop the currently running process.
Reversible: Yes — launchctl enable <domain>/<label>.
Use when: You want to permanently stop a service from auto-starting.
launchctl disable gui/$UID/com.example.helper
launchctl bootout <domain>/<label>Effect: Unloads the currently running job. Stops the process. The job will come back on next reboot UNLESS also disabled.
Reversible: Implicit — next reboot reloads.
Use when: You want to kill the running daemon right now but allow it to come back later.
launchctl bootout gui/$UID/com.example.helper
launchctl unload <plist-path>Legacy form of bootout. Takes a path instead of a domain/label. Still works on most macOS versions but deprecated; prefer bootout.
launchctl disable gui/$UID/com.example.helper # don't reload on next boot
launchctl bootout gui/$UID/com.example.helper # kill the running process
For system daemons:
sudo launchctl disable system/com.example.daemon
sudo launchctl bootout system/com.example.daemon
RunAtLoad=true + KeepAlive=falseRun once at load (typically at user login or system boot). If the process exits, don't restart.
RunAtLoad=true + KeepAlive=trueRun at load, restart whenever it exits — "always running" service.
RunAtLoad=false + StartCalendarIntervalDon't run at load. Run on a schedule. Equivalent to cron.
RunAtLoad=false + WatchPathsDon't run at load. Run when a specific path is written to. Used for "watch this file for changes".
ThrottleIntervalMinimum seconds between restarts. Default 10. If a job crashes faster than this, launchd will throttle it ("service throttled by N seconds" in the log). High throttling = the daemon is crash-looping.
In rough order of frequency:
plutil -lint /path/to/plist validates structureroot:wheel with mode 644; LaunchAgents owned by the user (or root)chmod 644 on the plist itselfspctl --addCheck load errors:
launchctl print gui/$UID/com.example.helper # detailed state
launchctl print-disabled gui/$UID | grep example # is it disabled?
log show --predicate 'process == "launchd"' --last 1h --style compact | grep example
On Apple Silicon, kernel extensions (kexts) are deprecated. Most kernel-level integrations have moved to System Extensions — daemons in /Library/SystemExtensions/ that run in user-mode but have privileged kernel APIs available via XPC.
Key differences:
| Property | Kext | System Extension |
|---|---|---|
| Lives in | /Library/Extensions |
/Library/SystemExtensions/<UUID>/<name>.systemextension |
| Loads via | kextd | sysextd |
| Requires reboot | Often | Usually not |
| Apple Silicon | Limited (deprecated) | Fully supported |
| Signing | Notarized + user approved | Notarized + user approved + Family-specific entitlements |
Inventory:
systemextensionsctl list
Disable via the system extension's app removing it, or:
systemextensionsctl uninstall <team-id> <bundle-id>
# Print all loaded jobs in user domain
launchctl print gui/$UID | head -40
# Print all loaded jobs in system domain
sudo launchctl print system | head -40
# Specific job's state
launchctl print gui/$UID/com.example.helper
# What's currently disabled?
launchctl print-disabled gui/$UID
sudo launchctl print-disabled system
# Validate a plist
plutil -lint /Library/LaunchAgents/com.example.helper.plist
# Convert plist to readable format
plutil -convert xml1 -o - /Library/LaunchAgents/com.example.helper.plist
# Watch launchd's log for a specific job
log stream --predicate 'process == "launchd" AND eventMessage CONTAINS "com.example"'
scripts/startup-audit.sh — inventory all launchd jobsscripts/safe-disable-startup.sh — disable + bootout in one step, reversiblewindows-ops/references/startup-mechanisms.mdtcc-mechanics.md