How to Configure launchd, Custom Daemons, and LaunchAgents in macOS Terminal

Architectural Foundation of launchd and Domain Targets in macOS

In macOS, launchd serves as the central init system and service management daemon, executing as Process ID 1 (PID 1). Designed by Apple to consolidate and supersede legacy Unix utilities—such as init, rc scripts, cron, at, and inetd—launchd unifies system bootstrap management, scheduled background automation, and on-demand socket activation under a declarative XML architecture.

A fundamental concept within modern launchd architecture is the Domain Target hierarchy. Every background task operates within an isolated security and execution domain. System-wide daemons run inside the system domain without user interface access, while user-specific automation executes inside the gui/<uid> (interactive graphical session) or user/<uid> (headless background session) domains. Differentiating between LaunchDaemons and LaunchAgents is essential for proper privilege escalation and security compliance.

Configuration Type Filesystem Storage Location Execution Context & Domain Privilege Level Access to GUI / WindowServer
User LaunchAgent ~/Library/LaunchAgents/ gui/<uid> (Per logged-in user) Standard User Yes (Displays alerts, UI dialogs)
Global LaunchAgent /Library/LaunchAgents/ gui/<uid> (All GUI login sessions) Logged-in User Yes (Instantiated on each user login)
System LaunchDaemon /Library/LaunchDaemons/ system (Bootstrapped at boot) root or specified system user No (Completely detached from GUI)
Apple System Agent /System/Library/LaunchAgents/ OS-protected system agents System / User Protected by System Integrity Protection (SIP)
Apple System Daemon /System/Library/LaunchDaemons/ OS-protected core services root Protected by System Integrity Protection (SIP)

Anatomy of an XML Property List Configuration

Every service managed by launchd is defined by an Apple XML property list (.plist). Configuration files must adhere strictly to Apple’s DTD schema, incorporating precise reverse-domain naming conventions and operational trigger keys.

<?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 reverse-domain service identifier -->
    <key>Label</key>
    <string>com.example.system.maintenance</string>

    <!-- Array of executable path and command-line arguments -->
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/maintenance_script.sh</string>
        <string>--full-cleanup</string>
    </array>

    <!-- Execute immediately upon service bootstrap -->
    <key>RunAtLoad</key>
    <true/>

    <!-- Periodic timer: Execute every 3600 seconds (1 hour) -->
    <key>StartInterval</key>
    <integer>3600</integer>

    <!-- Working directory for process execution -->
    <key>WorkingDirectory</key>
    <string>/var/log/maintenance</string>

    <!-- Diagnostic stdout and stderr file logging -->
    <key>StandardOutPath</key>
    <string>/tmp/maintenance.stdout.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/maintenance.stderr.log</string>

    <!-- Prevent launchd from restarting process if it exits cleanly (code 0) -->
    <key>KeepAlive</key>
    <dict>
        <key>SuccessfulExit</key>
        <false/>
    </dict>
</dict>
</plist>

Step-by-Step Implementation: Creating and Deploying a LaunchAgent

Step 1: Construct the Executable Script

Create an automated maintenance script on your local filesystem and ensure it possesses executable POSIX permissions:

# Create directory structure
mkdir -p ~/scripts

# Write a basic backup script
cat << 'EOF' > ~/scripts/nightly_sync.sh
#!/bin/bash
set -euo pipefail
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Initialising backup sequence..."
rsync -avz --delete ~/Documents/ /Volumes/BackupDrive/Documents/
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup sequence completed."
EOF

# Grant execution rights
chmod +x ~/scripts/nightly_sync.sh

Step 2: Compose the User LaunchAgent Configuration

Create the XML definition within the user’s private agent catalogue under ~/Library/LaunchAgents/:

cat << 'EOF' > ~/Library/LaunchAgents/com.user.nightlysync.plist
<?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.user.nightlysync</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>-c</string>
        <string>$HOME/scripts/nightly_sync.sh</string>
    </array>
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>2</integer>
        <key>Minute</key>
        <integer>30</integer>
    </dict>
    <key>StandardOutPath</key>
    <string>/tmp/nightlysync.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/nightlysync.err</string>
</dict>
</plist>
EOF

Step 3: Validate File Permissions and Schema Integrity

launchd rejects configuration files that permit write access to secondary non-root users. Validate file security and XML formatting using plutil:

# Set strict file permissions (Owner read/write only)
chmod 644 ~/Library/LaunchAgents/com.user.nightlysync.plist

# Verify XML syntax and schema compliance
plutil -lint ~/Library/LaunchAgents/com.user.nightlysync.plist

Managing Services with Modern launchctl Subcommands

While legacy tutorials advocate launchctl load and launchctl unload, Apple deprecated these commands in favour of explicit domain-aware verbs: bootstrap, bootout, enable, and kickstart.

# Determine current user ID
UID=$(id -u)

# 1. Bootstrap (load) the LaunchAgent into the graphical session domain
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.user.nightlysync.plist

# 2. Inspect active service state and exit status
launchctl print gui/$UID/com.user.nightlysync

# 3. Manually trigger execution without waiting for scheduled timer
launchctl kickstart -k gui/$UID/com.user.nightlysync

# 4. Bootout (unload) the service from the domain target
launchctl bootout gui/$UID/com.user.nightlysync

For system-level LaunchDaemons located in /Library/LaunchDaemons/, commands must execute with root privileges against the system domain:

# Enforce root ownership (mandatory for LaunchDaemons)
sudo chown root:wheel /Library/LaunchDaemons/com.example.daemon.plist
sudo chmod 644 /Library/LaunchDaemons/com.example.daemon.plist

# Bootstrap system daemon
sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.daemon.plist

# Inspect daemon runtime metrics
sudo launchctl print system/com.example.daemon

Troubleshooting launchd Failures and Execution Errors

Observed Error / Code Probable Root Cause Technical Diagnostic Method Resolution Procedure
Exit Code 78 (EX_CONFIG) Configuration or permission error; executable lacks +x flag or invalid XML Run launchctl print gui/$UID/label; check last exit code Run chmod +x on binary; validate plist syntax using plutil -lint
Exit Code 126 / 127 Command cannot execute or interpreter not found in default minimal PATH Review stderr output in StandardErrorPath log file Specify absolute paths for all utilities (e.g., /bin/bash, /usr/bin/rsync)
Bootstrap failed: 5: Input/output error Service is marked disabled in launchd override database or invalid domain Run launchctl print-disabled gui/$UID Enable service using launchctl enable gui/$UID/label prior to bootstrap
Operation not permitted (TCC Fault) Daemon lacks Transparency, Consent, and Control permissions (Full Disk Access) Inspect unified logging: log show --predicate 'subsystem == "com.apple.TCC"' Grant Full Disk Access to target executable in System Settings > Privacy & Security
launchctl: Path had bad ownership/permissions Plist file is group-writable or owned by incorrect user Run ls -l /Library/LaunchDaemons/ Execute sudo chown root:wheel <file> && sudo chmod 644 <file>

Get the best tech tips delivered straight to your inbox.

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