When working with launchd on macOS, you might encounter situations where you need to reference environment variables in your plist configuration. While launchd doesn't directly expand shell variables in ProgramArguments, there are several effective workarounds.
The main issue arises because launchd plist files are XML documents parsed by the system, not shell scripts. When you try to use something like $HOME
in ProgramArguments, it's treated as a literal string rather than being expanded:
<array>
<string>/bin/sh</string>
<string>$HOME/bin/script.sh</string> // Won't work!
</array>
Here are three reliable approaches to solve this problem:
1. Using Absolute Paths
The simplest solution is to use the full path:
<array>
<string>/Users/yourusername/bin/attach-devroot.sh</string>
</array>
2. Environment Variables Section
Add an EnvironmentVariables dictionary to your plist:
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>/Users/yourusername</string>
</dict>
3. Wrapper Script Approach
Create a wrapper script that handles the environment variables:
#!/bin/sh
$HOME/bin/attach-devroot.sh
Then reference the wrapper in your plist:
<array>
<string>/path/to/wrapper.sh</string>
</array>
You can force shell expansion by explicitly calling the shell:
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>$HOME/bin/script.sh</string>
</array>
- Always use absolute paths when possible
- Consider using
~/Library/LaunchAgents
for user-specific agents - Test your plist with
launchctl unload
andlaunchctl load
- Check logs with
console
app if things don't work
Here's a full working example that combines several techniques:
<?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.myapp</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>source ~/.profile && $HOME/bin/myapp --daemon</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/myapp.log</string>
<key>StandardErrorPath</key>
<string>/tmp/myapp.err</string>
</dict>
</plist>
When working with launchd plist files on macOS, you might encounter situations where you need to reference environment variables like $HOME in your ProgramArguments. However, launchd doesn't directly expand environment variables in the plist XML file.
Unlike shell scripts, launchd plist files don't perform environment variable expansion by default. The XML parser treats the text literally, so $HOME/bin/attach-devroot.sh
won't be expanded to /Users/yourusername/bin/attach-devroot.sh
.
Here are three effective approaches to handle environment variables in your launchd configuration:
1. Using a Wrapper Shell Script
Create a simple shell script that expands the variables and then executes your command:
#!/bin/sh
exec "$HOME/bin/attach-devroot.sh"
Then reference this wrapper in your plist:
<array>
<string>/path/to/wrapper.sh</string>
</array>
2. Setting EnvironmentVariables in the plist
You can define environment variables directly in the plist:
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>/Users/yourusername</string>
</dict>
3. Using launchctl setenv Before Loading
Set the environment variable before loading your plist:
launchctl setenv HOME $HOME
launchctl load ~/Library/LaunchAgents/your.plist
For maximum reliability, I recommend combining methods 2 and 3:
<?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.yourprogram</string>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>/Users/yourusername</string>
</dict>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>exec $HOME/bin/yourscript.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
- Use
launchctl getenv HOME
to verify environment variables - Check system logs with
log show --predicate 'subsystem == "com.apple.xpc.launchd"'
- Test your script directly in Terminal first
- Remember that launchd runs with a different environment than your user shell