How to Run a Process as a Specific User on macOS Startup Using launchd


2 views

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:

  1. A LaunchDaemon to trigger execution at startup
  2. 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