skip to content

On modern macOS, how do launchctl bootstrap and bootout differ from the older launchctl load and unload, and what does a service target such as system/com.example.job or gui/501/com.example.agent identify?

level: seniorimportance: should knowfreq 50%

answer

  1. the old commands guessed, the new ones are told
  2. jobs live in containers with lifetimes
  3. system, user, gui, pid
  4. one operation lasts past a reboot
  5. the plist comes back at boot unless policy says no

basics

~20 s

bootstrap and bootout are the domain-aware commands: you name the domain explicitly, such as system or gui/501, and launchctl loads or removes the job there. load and unload are the legacy spelling that infers the domain from context, and they are kept only for compatibility.

solid answer

~40 s

launchctl was rewritten around an explicit domain model, so every operation now names a target. A *domain target* is something like `system` (the boot-time domain), `user/501` (a user's domain) or `gui/501` (that user's GUI login session); a *service target* is a domain plus a job label, `system/com.example.job`. `bootstrap <domain> <plist-path>` loads a job into a named domain and `bootout` removes it, replacing `load`/`unload`, which guessed the domain from whether you were root and which directory the plist sat in — exactly the ambiguity that produced "it works with sudo but not without". The modern commands also fail loudly with a real error instead of silently doing nothing. Alongside them, `launchctl kickstart -k <service-target>` restarts a running service, `launchctl print <target>` dumps its state, and `enable`/`disable` set persistent policy rather than touching the current session.

code

bash · 5 lines
bash
sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.job.plist
sudo launchctl kickstart -k system/com.example.job
sudo launchctl print system/com.example.job
sudo launchctl bootout system/com.example.job
sudo launchctl disable system/com.example.job

go deeper

for a junior

Know that launchctl bootstrap loads a job and bootout removes it, that load and unload are the older equivalents, and that a daemon needs root while an agent does not.

for a middle

Explain the domain model: system, user, gui and pid domains with different lifetimes, the domain-plus-label service target, and why the old commands' inference caused ambiguity.

for a senior

Draw the operational line between session state and persistent policy — bootout removes the job now, disable survives reboot and blocks future bootstraps — and use kickstart and print instead of guessing at state.

for a principal

Set the standard for how software is installed and retired on managed Macs: whether helpers ship as installer-dropped plists or app-registered services, and how a decommissioning runbook makes a service stay gone rather than merely stop.

## The old model and why it was replaced Original `launchctl load /Library/LaunchDaemons/foo.plist` had no way to say *where* the job should go. launchctl inferred it: root got the system domain, a normal user got their own. That inference is fine until it is not — a `sudo launchctl load ~/Library/LaunchAgents/foo.plist` puts a job somewhere nobody intended, and the classic symptom set ("loading it prints nothing but nothing runs", "it works from a terminal but not from the installer") comes straight out of the guessing. The rewritten launchctl removed the guess. ## Domain targets A domain is a container for jobs with a lifetime of its own: - `system` — exists from boot to shutdown; where Launch Daemons live; requires root. - `user/<uid>` — that user's domain. - `gui/<uid>` — that user's graphical login session; the right domain for an agent that needs the window server. - `pid/<pid>` — the domain belonging to a single running process, which is how per-process XPC services are addressed. Append a job label to a domain and you have a *service target*: `system/com.example.job`, `gui/501/com.example.agent`. Almost every modern subcommand takes one of these two forms, which is what makes the commands unambiguous. ## The verbs - `bootstrap <domain-target> <path-to-plist>` — load a job into that domain. Root and the system domain for a daemon; your own uid for an agent. - `bootout <domain-target> <path>` or `bootout <service-target>` — remove it. This also terminates the running process. - `kickstart -k <service-target>` — start it now, and with `-k` kill the current instance first. This is the honest "restart the service" command. - `print <target>` — the diagnostic dump: state, PID, last exit status, the resolved program arguments, endpoints. Far more useful than the old list output. - `enable` / `disable <service-target>` — persistent policy. ## enable/disable versus bootstrap/bootout This is the distinction that separates people who have operated macOS from people who have read a blog post. `bootout` affects the *current* domain: the job is gone now, and at the next boot or login it comes back, because the plist is still on disk and launchd loads it again. `disable` writes into launchd's persistent override database and survives reboot: a disabled service will not be bootstrapped, and an attempt to bootstrap it fails saying so. That last part explains a genuinely confusing failure — bootstrapping a service that somebody disabled months ago errors out even though the plist is perfect, and the fix is `launchctl enable` on the service target, not another look at the XML. Historically `load -w` and `unload -w` edited a `Disabled` key, and people still reach for `-w` out of habit; the persistent state now lives in launchd's own database rather than in your plist, which is why hand-editing a `Disabled` key is no longer the answer. ## Which to use Use the modern verbs. `load` and `unload` still work and you will see them everywhere in older documentation and installer scripts, but they are legacy: they infer what you should be stating, and they are quieter about failure. When you write an installer or a runbook today, `bootstrap system /Library/LaunchDaemons/com.example.job.plist` says exactly what it does, and its exit status means something. ## The interview point What is being tested is whether you understand that launchd organises jobs into domains with different lifetimes and privileges, and that a job's presence in a domain right now is a different thing from the persistent policy about whether it may be there at all. Candidates who only know `load`/`unload` usually also believe unloading a job keeps it off across reboots — and that belief is how a service everyone thought was disabled comes back after a maintenance window.

  • You bootout a daemon during an incident and it is running again the next morning. Why?
    bootout only removes it from the current system domain. The plist is still in /Library/LaunchDaemons, so the next boot loads it again. To keep it off you need persistent policy — launchctl disable on the service target — or the plist removed from disk. Conflating the two is a classic post-maintenance surprise.
  • A bootstrap fails with a message about the service being disabled, though the plist is correct. What do you do?
    Somebody set persistent policy against it. Run launchctl enable on the service target, then bootstrap again. The disabled state lives in launchd's override database rather than in your plist, so re-reading or re-writing the XML will not clear it — and neither will editing a Disabled key by hand.
  • Why does gui/501 exist as a separate domain from user/501?
    Because a user can have state without a graphical login session — an SSH session, for example. The gui domain is the one bound to the Aqua session and the window server, so an agent that must draw something or talk to the session belongs there, while purely background per-user work can live in the user domain.

saying these in an interview costs you the question

  • Thinks unload or bootout keeps a service off across reboots
  • Uses sudo launchctl on a user agent's plist
  • Believes load and bootstrap are just spellings of one thing
  • Edits a Disabled key in the plist to keep a job off
  • Cannot say what the number in gui/501 means

context