How to Use the launchctl Command in macOS Terminal to Manage LaunchDaemons and User Agents

In macOS, process lifecycle management, background automation, and service scheduling are governed by launchd. Designed by Apple to consolidate and replace legacy Unix facilities—including init, cron, inetd, and rc.local—launchd serves as the primary root process (PID 1) across the operating system. To inspect, load, trigger, and debug these background tasks from the Terminal, system administrators and power users interface directly with launchctl. Understanding how to manage LaunchDaemons and LaunchAgents via modern launchctl subcommands is fundamental to managing system services and diagnosing rogue background processes in macOS.

Distinguishing LaunchDaemons from LaunchAgents

Background tasks in macOS are declared through XML Property List (.plist) files and fall into two distinct execution tiers: LaunchDaemons and LaunchAgents. The table below delineates the architectural differences between them:

Execution Tier Filesystem Storage Directory Security Context Operational Lifecycle
System LaunchDaemons /System/Library/LaunchDaemons/ Root (System-managed, SIP protected) Boots with macOS kernel; zero user GUI interaction.
Administrator LaunchDaemons /Library/LaunchDaemons/ Root or designated system user Runs at startup before login; headless background background services.
Global LaunchAgents /Library/LaunchAgents/ Active logged-in user Initialises when any user logs in; has access to the user GUI window server.
User-Specific LaunchAgents ~/Library/LaunchAgents/ Specific target user account Initialises strictly when that particular user logs in; personal scripts and user utilities.

Modern vs. Legacy launchctl Subcommands

Older macOS documentation frequently references legacy commands such as launchctl load and launchctl unload. In modern releases of macOS (macOS Monterey, Ventura, Sonoma, and Sequoia), Apple deprecated these in favour of domain-scoped targets utilizing bootstrap, bootout, kickstart, and print.

Modern Subcommand Target Domain Scope Operational Function Replaces Legacy Command
launchctl bootstrap <domain> <path> system or gui/<uid> Loads and registers a service definition into the specified execution domain. launchctl load
launchctl bootout <domain> <path> system or gui/<uid> Unregisters and terminates a running service from the domain. launchctl unload
launchctl kickstart -k <service-target> system/<label> or gui/<uid>/<label> Forces an immediate process restart or execution trigger. launchctl start
launchctl print <domain> system or gui/<uid> Prints real-time diagnostic telemetry, exit codes, and environment variables. launchctl list

Querying and Inspecting Active Services

Before altering any system configuration, identify the user domain ID and inspect existing loaded services.

1. Identifying the User Domain ID

User-level agents execute inside the GUI domain mapped to your user ID (UID). Retrieve your current UID with:

id -u

(Typically 501 for the primary administrative user account).

2. Inspecting Domain Services with launchctl print

To inspect all currently registered user LaunchAgents:

launchctl print gui/$(id -u)

To inspect detailed diagnostic telemetry for a specific individual service (such as an internal backup utility labelled com.company.backup):

launchctl print gui/$(id -u)/com.company.backup

This command outputs the current PID, binary program path, environment variables, execution limits, and the last process exit code (where 0 indicates a clean exit and non-zero values highlight runtime crashes).

Authoring a Custom LaunchAgent Property List

To automate a script on a schedule or ensure an internal service restarts upon unexpected termination, create a property list definition.

1. Creating the .plist File

Navigate to your user agents directory and create a new property list:

nano ~/Library/LaunchAgents/com.user.maintenance.plist

Populate the file with the following structured XML configuration:

<?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>
    <!-- Unique identifier label -->
    <key>Label</key>
    <string>com.user.maintenance</string>

    <!-- Binary executable and arguments -->
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/python3</string>
        <string>/Users/admin/scripts/cleanup.py</string>
    </array>

    <!-- Execution triggers -->
    <key>RunAtLoad</key>
    <true/>

    <!-- Repeat interval specified in seconds (3600 = 1 hour) -->
    <key>StartInterval</key>
    <integer>3600</integer>

    <!-- Standard input/output log paths -->
    <key>StandardOutPath</key>
    <string>/tmp/com.user.maintenance.stdout.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/com.user.maintenance.stderr.log</string>
</dict>
</plist>

2. Setting Proper File Permissions

macOS security modules will reject any property list with insecure permissions:

chmod 644 ~/Library/LaunchAgents/com.user.maintenance.plist

Loading and Managing Services via Modern launchctl

With the property list created and permissions verified, use modern domain targeting to control the service.

1. Bootstrapping the Service

To load and register the agent into the active user session:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.maintenance.plist

2. Forcing an Immediate Run with kickstart

Rather than waiting for the timer interval to elapse, trigger the process immediately to verify functionality:

launchctl kickstart -k gui/$(id -u)/com.user.maintenance

The -k switch terminates any currently running instance before launching a fresh execution.

3. Booting Out (Unloading) a Service

When updating a script or decommissioning a task, unregister it from memory:

launchctl bootout gui/$(id -u)/com.user.maintenance

Managing System-Wide LaunchDaemons with Elevated Privileges

For system-wide background processes running as root (such as database listeners or web services), property lists must be placed in /Library/LaunchDaemons/ with root ownership:

sudo chown root:wheel /Library/LaunchDaemons/com.company.service.plist
sudo chmod 644 /Library/LaunchDaemons/com.company.service.plist

To load the service into the system domain:

sudo launchctl bootstrap system /Library/LaunchDaemons/com.company.service.plist

To verify that the system daemon is active and running:

sudo launchctl print system/com.company.service

Diagnosing and Troubleshooting launchd Failures

When a background task fails to execute or enters a continuous crash-restart loop, evaluate these diagnostic checkpoints:

  • Exit Code 78 (EX_CONFIG): Indicates a permissions error. Ensure the target script or binary possesses executable permissions (chmod +x /path/to/script) and that parent directories are accessible by the service account.
  • macOS Full Disk Access and TCC Restrictions: If a script fails to access directories like ~/Documents, ~/Downloads, or removable disks, macOS Transparency, Consent, and Control (TCC) security has blocked access. You must grant Full Disk Access to the calling binary (e.g., /bin/bash or /usr/local/bin/python3) within System Settings > Privacy & Security > Full Disk Access.
  • Inspecting Live System Logs: Filter Apple Unified Logging in Terminal to watch launchd activity in real time:
    log stream --predicate 'process == "launchd"' --level debug
  • Unlocking Corrupted Service States: If bootstrap returns service already loaded but print shows it missing, force an unregister using:
    launchctl bootout gui/$(id -u)/com.user.maintenance 2>/dev/null || true

Get the best tech tips delivered straight to your inbox.

Join thousands of readers mastering Apple, Google, Microsoft, and Linux.