To run a script before boot up on macOS, create a plist file in /Library/LaunchDaemons/ with the key RunAtLoad set to true. Load it using sudo launchctl load. This ensures the service starts at system initialization, granting root privileges and running before user login.
Understanding Launch Daemons vs Agents
macOS uses launchd to manage background processes. It is crucial to distinguish between Launch Agents and Launch Daemons. Launch Agents run in the context of a specific user and start when that user logs in. They are stored in ~/Library/LaunchAgents/ or /Library/LaunchAgents/. However, for tasks that must execute before any user logs in, you need a Launch Daemon. These run as root and are placed in /Library/LaunchDaemons/. This distinction is vital because scripts requiring hardware access or network configuration early in the boot process often fail if loaded as agents.
Creating the Plist File
The core of this setup is the XML property list file. You must create a new file in /Library/LaunchDaemons/. For example, name it com.example.mybootscript.plist. The structure requires specific keys to function correctly during the boot phase. Below is a minimal template.
<?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.mybootscript</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/path/to/your/script.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
<key>StandardOutPath</key>
<string>/tmp/mybootscript.log</string>
<key>StandardErrorPath</key>
<string>/tmp/mybootscript_error.log</string>
</dict>
</plist>
Ensure the Label matches your script's purpose and follows reverse-DNS naming conventions. The ProgramArguments array must include the interpreter (like /bin/bash) as the first element, followed by the path to your actual script. The RunAtLoad key is mandatory for pre-boot execution. Logging paths are highly recommended for debugging, as standard output is not visible in the console during boot.
Setting Permissions and Ownership
macOS enforces strict security policies on files in /Library/LaunchDaemons/. If permissions are incorrect, launchd will silently ignore the file. You must ensure the file is owned by root and has read-only permissions for others.
- Create the plist file using a text editor like
nanoorvim. - Set the ownership to root:
sudo chown root:wheel /Library/LaunchDaemons/com.example.mybootscript.plist. - Set permissions to 644:
sudo chmod 644 /Library/LaunchDaemons/com.example.mybootscript.plist. - Ensure your actual script is executable:
sudo chmod +x /path/to/your/script.sh.
Failure to adhere to these permission standards is the most common reason for launch daemon failures. The system checks these attributes before loading the job. Additionally, verify that your script does not rely on GUI elements or user-specific environment variables, as these are not available in the daemon context.
Loading and Verifying the Service
Once the file is created and permissions are set, you can load it into the system. Use the following command to load the daemon:
sudo launchctl load /Library/LaunchDaemons/com.example.mybootscript.plist
If successful, there will be no output. To verify it is running, check the status:
sudo launchctl list | grep com.example.mybootscript
You should see an entry indicating the job is active. If you encounter errors, check the log files specified in the StandardOutPath and StandardErrorPath. Also, review /var/log/system.log for messages related to launchd. Common issues include syntax errors in the XML plist or missing executable bits on the script.
Troubleshooting Common Issues
If the script does not run, consider the following diagnostic steps. First, ensure the script path is absolute. Relative paths often fail in daemon contexts. Second, check if the script requires network connectivity. If so, add <key>NetworkState</key><true/> to the plist to delay execution until the network is ready. Third, verify that the script does not interact with the window server or require user input. Finally, remember that changes to launch daemons do not take effect until the next reboot or after unloading and reloading the service. Unlike some other services, launchd does not automatically detect file changes in real-time for daemons.
Frequently Asked Questions
Can I run a shell script directly without bash?
No, you must specify the interpreter in the ProgramArguments array. Even if your script has a shebang line, launchd requires the explicit path to the binary, such as /bin/bash or /usr/bin/python3, as the first argument.
Why is my launch daemon not showing up in launchctl list?
This usually indicates a permissions error or a malformed XML file. Check that the file is owned by root:wheel and has 644 permissions. Also, validate the XML syntax using plutil -lint to catch hidden formatting issues.
How do I stop a launch daemon from running?
Use sudo launchctl unload /Library/LaunchDaemons/com.example.mybootscript.plist. This removes the job from the current session but keeps the plist file on disk. To permanently disable it, you must delete or move the plist file.
Will this work on Apple Silicon Macs?
Yes, the mechanism is identical across Intel and Apple Silicon Macs. The only difference is the underlying architecture, which does not affect how launchd interprets plist files or manages system-level daemons.
Can I pass arguments to my script via the plist?
Yes, simply add additional <string> elements to the ProgramArguments array after the script path. Each string represents a separate argument passed to the script in order.