Running a process as a specific user during system startup (not login) on macOS can be tricky, especially when dealing with sensitive operations like keychain access. The launchd
system is the preferred method, but it requires careful configuration.
The macOS security model prevents LaunchDaemons from properly assuming user contexts. As noted in the man launchd
documentation, LaunchAgents are the only supported way to run processes in a user's environment.
Here's how to create a LaunchAgent that runs at startup while executing as a specific user:
Label
com.example.startupscript
ProgramArguments
/path/to/your/script.sh
RunAtLoad
KeepAlive
AbandonProcessGroup
For cases where you absolutely need root privileges but want to execute as a user:
Label
com.example.sudoscript
ProgramArguments
/usr/bin/sudo
-u
username
/path/to/your/script.sh
RunAtLoad
For keychain operations, you'll need to ensure the script properly accesses the user's keychain:
#!/bin/bash
security unlock-keychain -p "password" /Users/username/Library/Keychains/login.keychain-db
security default-keychain -s /Users/username/Library/Keychains/login.keychain-db
Place your LaunchAgent in ~/Library/LaunchAgents/
for user-specific agents or /Library/LaunchAgents/
for system-wide installation. Then load it with:
launchctl load ~/Library/LaunchAgents/com.example.startupscript.plist
To test immediately without reboot:
launchctl start com.example.startupscript
Check system logs with:
log show --predicate 'senderImagePath contains "com.example"' --last 1h
Or view specific process output:
tail -f /var/log/system.log
When dealing with system-level automation on macOS, you'll quickly encounter the distinction between LaunchDaemons
(system-wide) and LaunchAgents
(user-specific). The fundamental challenge arises when you need to execute processes in a user context without an active login session.
The system strictly separates privileges - LaunchDaemons
run as root and lack access to user environments, including keychain operations. As stated in man launchd
: attempting to setuid()
won't properly establish the user environment needed for operations like keychain access.
We'll implement a two-part solution:
- A
LaunchDaemon
to trigger execution at startup - A helper mechanism to properly establish user context
Create a plist at /Library/LaunchDaemons/com.example.userstartup.plist
:
Label
com.example.userstartup
ProgramArguments
/usr/bin/su
targetuser
-c
/path/to/your/script.sh
RunAtLoad
StandardErrorPath
/var/log/userstartup.err
StandardOutPath
/var/log/userstartup.log
Here's a sample script (script.sh
) that demonstrates keychain operations:
#!/bin/bash
# Set the keychain you want to modify
KEYCHAIN="login.keychain"
# Unlock the keychain first
security unlock-keychain -p "yourpassword" $HOME/Library/Keychains/$KEYCHAIN
# Example operation: list all certificates
security find-certificate -a -p $HOME/Library/Keychains/$KEYCHAIN > $HOME/certificates.pem
# Set as default keychain (only works in proper user context)
security default-keychain -s $HOME/Library/Keychains/$KEYCHAIN
Set proper permissions for the plist:
sudo chown root:wheel /Library/LaunchDaemons/com.example.userstartup.plist
sudo chmod 644 /Library/LaunchDaemons/com.example.userstartup.plist
For security, consider:
- Using
launchctl
wrappers instead of direct file execution - Storing sensitive information in the System Keychain rather than scripts
- Implementing proper error handling in your scripts
For modern macOS versions (10.10+), consider using launchctl bootstrap
:
sudo launchctl bootstrap user/$(id -u targetuser) /Library/LaunchAgents/com.example.userstartup.plist
Check logs with:
tail -f /var/log/userstartup.{log,err}
journalctl --user-unit=com.example.userstartup
Verify the environment with:
security authorizationdb read system.login.console