launchctl is the front end to launchd, the process that starts and supervises background services on a Mac — everything from system daemons to the login-item agents your own apps install. Two vocabularies both work: the older load/unload that most instructions online still use, and the newer bootstrap/bootout that give clearer errors. This covers the commands you actually reach for — listing, loading, restarting, and working out why a service won’t start — with both spellings where they differ. Everything below was run on macOS 26.5.2 against a throwaway agent, not any real service.
See what’s loaded: launchctl list
launchctl list prints every service loaded in your session — three columns, and the middle one is the part people miss:
$ launchctl list | head -1
PID Status Label
PID is the running process ID, or - if the service isn’t running right now. Label is the service’s reverse-DNS name, the same string from its plist. Status is the one that matters when something’s wrong: it’s the service’s last exit code. 0 means it ran and exited cleanly; anything non-zero means the last run failed, and that number is your first clue. Narrow the list to one service with grep:
$ launchctl list | grep myagent
12783 0 com.example.myagent
Load and unload an agent
A per-user service is a LaunchAgent: a plist in ~/Library/LaunchAgents. A minimal one names the service, the program to run, and asks launchd to start it on load:
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>Label</key> <string>com.example.myagent</string>
<key>ProgramArguments</key>
<array><string>/usr/local/bin/myscript</string></array>
<key>RunAtLoad</key> <true/>
</dict>
</plist>
The command most guides show still works:
launchctl load ~/Library/LaunchAgents/com.example.myagent.plist
launchctl unload ~/Library/LaunchAgents/com.example.myagent.plist
The newer form does the same job but takes a domain target — gui/<your-uid> for your logged-in session, where the UID is what id -u prints (usually 501 for the first account):
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.myagent.plist
launchctl bootout gui/$(id -u)/com.example.myagent
Note the asymmetry: bootstrap takes the path to the plist, but bootout takes the domain plus label, not a path. That trips people up. The payoff for the extra typing is that bootstrap reports failures instead of swallowing them — more on that below. System-wide services (LaunchDaemons in /Library/LaunchDaemons) use the system/ domain instead of gui/ and need sudo.
Start or restart on demand: kickstart
Loading a service isn’t the same as running it — a service set to run on a schedule or a socket sits loaded but idle. To run it now, or to bounce one that’s misbehaving, kickstart the label. The -k flag kills a running copy first, so it’s a clean restart:
launchctl kickstart gui/$(id -u)/com.example.myagent # start now
launchctl kickstart -k gui/$(id -u)/com.example.myagent # restart
This is the modern answer to “restart a service” — there’s no launchctl restart verb, and the old load/unload two-step is what kickstart -k replaced.
Why a service won’t start
Start with launchctl list. A loaded service that keeps failing shows a - for PID (not running) and its exit code in the Status column:
$ launchctl list | grep myagent
- 1 com.example.myagent
A non-running service with a non-zero status ran and quit with that code — here 1. For the fuller picture, launchctl print on the domain target spells out the state and the last exit code directly:
$ launchctl print gui/$(id -u)/com.example.myagent
state = not running
program = /usr/local/bin/myscript
runs = 1
last exit code = 1
The other common failure happens at load time, and it’s the one worth recognizing because the message is so opaque. Point bootstrap at a plist that doesn’t exist, has a syntax error, or is already loaded, and launchd answers:
$ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/typo.plist
Bootstrap failed: 5: Input/output error
Try re-running the command as root for richer errors.
5: Input/output error is launchd’s catch-all for “couldn’t load this,” not an actual disk problem. In practice it’s almost always one of three things: the path is wrong, the plist’s XML doesn’t parse, or a service with that label is already bootstrapped (boot it out first, then bootstrap again). This is where bootstrap earns its place over load — the legacy launchctl load often exits 0 and silently does nothing on the same broken plist, giving you no error to search for at all.
Old vocabulary, new vocabulary
Both are current — load/unload aren’t going away, and the vast majority of setup instructions still use them, so it’s worth knowing the map rather than picking a side:
| Task | Legacy | Modern |
|---|---|---|
| Load an agent | launchctl load path.plist | launchctl bootstrap gui/UID path.plist |
| Unload an agent | launchctl unload path.plist | launchctl bootout gui/UID/label |
| Start / restart now | unload then load | launchctl kickstart -k gui/UID/label |
| Inspect one service | launchctl list label | launchctl print gui/UID/label |
The practical rule: use whichever the instructions you’re following use, but when a load mysteriously does nothing, re-run it as bootstrap to get the error load hid. Read the Status column in launchctl list first, reach for launchctl print when you need the last exit code in plain sight, and kickstart -k to bounce a service without unloading it. For finding and ending a stuck process by name or port rather than by launchd label, kill, pkill, killall, and the port one-liner cover that side.