LaunchAgent Path Mismatch Debugging
LaunchAgent Path Mismatch Debugging: When Your Daemon Refuses to Start
LaunchAgent path mismatches are among the most frustrating macOS debugging experiences precisely because they fail silently. The agent loads, the system acknowledges the plist, and nothing happens — no error dialog, no obvious log entry, just a process that never materializes. Understanding why this happens and how to systematically diagnose it will save hours of guesswork.
What a Path Mismatch Actually Means
When launchd reads a plist file, it trusts the ProgramArguments array completely. If the binary path specified doesn’t exist, isn’t executable, or points to the wrong architecture, launchd will attempt to start the job and immediately record a failure — but it won’t tell you proactively. The mismatch can occur at several layers:
- The binary was moved or deleted after the plist was installed
- A package manager (Homebrew, nix, conda) relocated the binary during an upgrade
- The path contains an unresolved symlink that breaks under a different user context
- The plist was copied from another machine with hardcoded absolute paths
- An agent installed under one user’s home directory references a path valid only for another user
The last point matters for autonomous agents: if a LaunchAgent plist is installed in /Library/LaunchAgents/ (system-wide) but the ProgramArguments path starts with /Users/alice/..., it will fail for every user except alice — and silently succeed for alice in ways that mask the real problem.
Immediate Diagnostic Commands
Start with launchctl to inspect the loaded state:
# List all loaded agents and check exit codes
launchctl list | grep com.yourorg.agent
# Expected output columns: PID, LastExitStatus, Label
# A LastExitStatus of 78 means misconfiguration
# A LastExitStatus of 2 often means the binary wasn't found
A LastExitStatus of 78 (EX_CONFIG) is the canonical signal for a configuration problem, including path issues. Status 2 frequently indicates the executable couldn’t be located or executed.
Pull the detailed job dictionary:
launchctl print gui/$(id -u)/com.yourorg.agent
Look at the program or program arguments field in the output. Compare it character-by-character against what actually exists on disk:
ls -la /path/to/your/binary
file /path/to/your/binary
The file command is underused here. It will tell you if the binary is an Intel-only Mach-O on an Apple Silicon machine, which produces a path mismatch in effect even if the file exists.
Reading the Unified Log
Console.app is slow for this work. Use log from the command line with a time filter:
log show --predicate 'subsystem == "com.apple.launchd"' \
--style syslog \
--last 5m \
| grep -i "com.yourorg.agent"
For a live tail during a launchctl load or launchctl kickstart:
log stream --predicate 'subsystem == "com.apple.launchd"' \
--style syslog \
| grep -i "yourorg"
You’re looking for entries containing posix_spawn, execve failed, or 13: Permission denied. A permission denial (errno 13) is a path mismatch variant: the file exists but isn’t executable, or sandboxing is blocking access.
Validating the Plist Before Loading
Run plutil to catch structural errors first:
plutil -lint ~/Library/LaunchAgents/com.yourorg.agent.plist
Then extract and verify the actual path the plist specifies:
/usr/libexec/PlistBuddy -c "Print :ProgramArguments:0" \
~/Library/LaunchAgents/com.yourorg.agent.plist
Cross-check with which and realpath:
realpath $(which yourbinary)
If the plist contains /usr/local/bin/yourbinary but realpath resolves to /opt/homebrew/bin/yourbinary via a symlink chain, you’ve found your mismatch. On Apple Silicon Macs with Homebrew, /usr/local is often a legacy path that no longer holds the canonical binary.
Symlink Chains and Environment Context
LaunchAgents run with a minimal environment. The PATH variable is not your shell’s PATH. If your ProgramArguments specifies just yourbinary without a full path, launchd will look in its own default path, which typically doesn’t include /opt/homebrew/bin.
Always use absolute paths in ProgramArguments. Verify the path resolves correctly under the launch context by temporarily adding an EnvironmentVariables block:
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
</dict>
This is a diagnostic step, not a permanent fix. Once you confirm the binary runs, update ProgramArguments to use the absolute resolved path instead.
The Reload Trap
A common compounding mistake: editing the plist on disk without unloading and reloading the agent. launchd caches the job definition. Your edits to the file are invisible until you explicitly reload:
launchctl unload ~/Library/LaunchAgents/com.yourorg.agent.plist
launchctl load ~/Library/LaunchAgents/com.yourorg.agent.plist
On macOS 13 and later, prefer the bootstrap/bootout pattern:
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.yourorg.agent.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.yourorg.agent.plist
After reloading, immediately check launchctl list again for the exit status. A path fix that doesn’t reset the exit code to - (running) or 0 (exited cleanly) means the mismatch is still present.
Systematic Resolution Checklist
- Confirm the binary exists:
ls -la <path> - Confirm it’s executable:
test -x <path> && echo ok - Confirm the architecture matches:
file <path>vsuname -m - Confirm the path is absolute in the plist, not a shell alias or relative path
- Confirm symlinks resolve correctly:
realpath <path> - Confirm the plist owner matches the domain: user plist in
gui/UID, system plist insystem/ - Unload, edit, reload — in that order, every time
Path mismatches are deterministic failures. They respond to methodical verification rather than intuition. The unified log, launchctl print, and realpath together give you everything needed to trace the exact point of divergence between what the plist expects and what the filesystem provides.
Write a comment