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> |