Skip to main content
โšก Calmops

Run a Program at Startup on macOS with launchd

Introduction

launchd is macOS’s service management framework โ€” the equivalent of systemd on Linux. It starts and manages background processes, daemons, and agents. To run a program automatically at startup (before or after login), you create a property list (.plist) file and register it with launchctl.

LaunchDaemons vs LaunchAgents

LaunchDaemons LaunchAgents
Location /Library/LaunchDaemons/ /Library/LaunchAgents/ or ~/Library/LaunchAgents/
Runs as root (or specified user) Logged-in user
Starts when System boot (before login) User logs in
Requires sudo Yes No
Use for System services, servers User-level apps, tools

Creating a LaunchDaemon (System-Wide Service)

Step 1: Create the plist file

sudo vim /Library/LaunchDaemons/com.example.myprogram.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>
    <!-- Unique identifier (reverse domain notation) -->
    <key>Label</key>
    <string>com.example.myprogram</string>

    <!-- Program and arguments -->
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/myprogram</string>
        <string>--config</string>
        <string>/etc/myprogram/config.yaml</string>
    </array>

    <!-- Start at boot -->
    <key>RunAtLoad</key>
    <true/>

    <!-- Restart if it crashes -->
    <key>KeepAlive</key>
    <true/>

    <!-- Log stdout and stderr -->
    <key>StandardOutPath</key>
    <string>/var/log/myprogram.log</string>
    <key>StandardErrorPath</key>
    <string>/var/log/myprogram.error.log</string>

    <!-- Working directory -->
    <key>WorkingDirectory</key>
    <string>/var/lib/myprogram</string>
</dict>
</plist>

Step 2: Set correct permissions

sudo chown root:wheel /Library/LaunchDaemons/com.example.myprogram.plist
sudo chmod 644 /Library/LaunchDaemons/com.example.myprogram.plist

Step 3: Load the service

# Load and start immediately
sudo launchctl load /Library/LaunchDaemons/com.example.myprogram.plist

# macOS 10.11+ (El Capitan and later) โ€” preferred
sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.myprogram.plist

Creating a LaunchAgent (User-Level Service)

For user-level services (no sudo required):

mkdir -p ~/Library/LaunchAgents
vim ~/Library/LaunchAgents/com.example.mytool.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.example.mytool</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Users/username/bin/mytool</string>
        <string>--daemon</string>
    </array>

    <key>RunAtLoad</key>
    <true/>

    <key>KeepAlive</key>
    <true/>

    <key>StandardOutPath</key>
    <string>/Users/username/Library/Logs/mytool.log</string>

    <key>StandardErrorPath</key>
    <string>/Users/username/Library/Logs/mytool.error.log</string>
</dict>
</plist>
# Load (no sudo needed for user agents)
launchctl load ~/Library/LaunchAgents/com.example.mytool.plist

Managing Services with launchctl

# Load a service (registers and starts it)
sudo launchctl load /Library/LaunchDaemons/com.example.myprogram.plist

# Unload a service (stops and unregisters it)
sudo launchctl unload /Library/LaunchDaemons/com.example.myprogram.plist

# Start a loaded service
sudo launchctl start com.example.myprogram

# Stop a running service
sudo launchctl stop com.example.myprogram

# List all loaded services
launchctl list

# Check a specific service
launchctl list | grep myprogram

# Print service info
sudo launchctl print system/com.example.myprogram

Modern launchctl (macOS 10.11+)

# Bootstrap (load + enable)
sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.myprogram.plist

# Bootout (unload + disable)
sudo launchctl bootout system/com.example.myprogram

# Enable/disable without starting
sudo launchctl enable system/com.example.myprogram
sudo launchctl disable system/com.example.myprogram

# Kickstart (force start)
sudo launchctl kickstart -k system/com.example.myprogram

Viewing Logs

# Follow the log in real-time
tail -f /var/log/myprogram.log
tail -f /var/log/myprogram.error.log

# View system logs for the service
log show --predicate 'process == "myprogram"' --last 1h

# Stream live logs
log stream --predicate 'process == "myprogram"'

Advanced plist Options

Run on a Schedule (Cron-like)

<!-- Run every hour -->
<key>StartInterval</key>
<integer>3600</integer>

<!-- Run at specific times (like cron) -->
<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key>
    <integer>2</integer>
    <key>Minute</key>
    <integer>30</integer>
</dict>

<!-- Run at multiple times -->
<key>StartCalendarInterval</key>
<array>
    <dict>
        <key>Hour</key><integer>9</integer>
        <key>Minute</key><integer>0</integer>
    </dict>
    <dict>
        <key>Hour</key><integer>17</integer>
        <key>Minute</key><integer>0</integer>
    </dict>
</array>

Environment Variables

<key>EnvironmentVariables</key>
<dict>
    <key>PATH</key>
    <string>/usr/local/bin:/usr/bin:/bin</string>
    <key>APP_ENV</key>
    <string>production</string>
    <key>LOG_LEVEL</key>
    <string>info</string>
</dict>

Conditional KeepAlive

<!-- Only restart on non-zero exit code -->
<key>KeepAlive</key>
<dict>
    <key>SuccessfulExit</key>
    <false/>
</dict>

<!-- Restart with throttle (wait 10s between restarts) -->
<key>ThrottleInterval</key>
<integer>10</integer>

Run as Specific User

<!-- For LaunchDaemons only -->
<key>UserName</key>
<string>www</string>
<key>GroupName</key>
<string>www</string>

Troubleshooting

Service won’t start

# Check exit code (non-zero = error)
launchctl list | grep myprogram
# Output: PID  ExitCode  Label
# -1 means not running, exit code shows why

# Check error log
cat /var/log/myprogram.error.log

# Validate plist syntax
plutil -lint /Library/LaunchDaemons/com.example.myprogram.plist

Permission issues

# Daemon plist must be owned by root:wheel
sudo chown root:wheel /Library/LaunchDaemons/com.example.myprogram.plist
sudo chmod 644 /Library/LaunchDaemons/com.example.myprogram.plist

# The program itself must be executable
chmod +x /usr/local/bin/myprogram

Common exit codes

Exit Code Meaning
0 Success (normal exit)
1 General error
78 Configuration error
126 Permission denied
127 Command not found

Resources

Comments