LaunchAgent Path Mismatch Debugging
LaunchAgent Path Mismatch Debugging: When Your Agent Won’t Load
LaunchAgents are one of macOS’s most reliable persistence mechanisms — until they aren’t. A path mismatch between the .plist definition and the actual binary location is one of the most common and quietly frustrating failure modes operators encounter. The agent silently fails to load, no obvious error surfaces in the GUI, and launchctl gives you just enough information to be confused. This post walks through the exact diagnostic chain to identify and resolve path mismatches, covering both the classical per-user LaunchAgent pattern and the system-level LaunchDaemon variant.
Understanding the Failure Mode
When launchd attempts to load an agent, it reads the ProgramArguments key (or Program key) from the plist and resolves the path at load time. If the binary doesn’t exist at that path, or if permissions prevent launchd from executing it, the job is registered but immediately enters a failed state. Critically, launchd will not always log this failure visibly — it depends on macOS version and whether the StandardErrorPath key is configured.
A typical broken plist looks like this:
<?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.myagent</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/myagent</string>
<string>--config</string>
<string>/etc/myagent/config.yaml</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
If the binary was installed to /opt/homebrew/bin/myagent (common on Apple Silicon Macs) instead of /usr/local/bin/myagent, the agent loads into launchctl successfully but never executes.
Step 1: Confirm the Agent Is Registered
Start by checking whether launchd has even parsed the plist:
launchctl list | grep com.example.myagent
A registered-but-failed job looks like:
- 78 com.example.myagent
The middle column is the exit code. 78 is EX_CONFIG — a configuration error, almost always indicating a path problem. Exit code 2 often means the binary itself exited with an error immediately, which is a different problem. Exit code - means the process has never started.
For a more detailed picture:
launchctl print gui/$(id -u)/com.example.myagent
Look for the last exit code and state fields. A state of waiting with a non-zero last exit code is your confirmation.
Step 2: Verify the Binary Path
With the label confirmed as broken, resolve the actual binary location:
# Check what the plist declares
/usr/libexec/PlistBuddy -c "Print :ProgramArguments:0" \
~/Library/LaunchAgents/com.example.myagent.plist
# Check where the binary actually lives
which myagent
ls -la /usr/local/bin/myagent 2>&1
ls -la /opt/homebrew/bin/myagent 2>&1
On Apple Silicon Macs, the Homebrew prefix shifted from /usr/local to /opt/homebrew. This single architectural change accounts for a large proportion of path mismatch incidents on M-series hardware. Always verify the prefix:
brew --prefix
Step 3: Check Permissions and Executable Bit
Even a correct path can cause a silent failure if permissions are wrong:
ls -la $(which myagent)
The binary needs execute permission for the user running the agent. For a user LaunchAgent, that’s your own UID. For a LaunchDaemon, it’s typically root. A common mistake is installing a binary with 644 permissions instead of 755.
Also check ownership:
stat -f "%Su:%Sg %Mp%Lp" /opt/homebrew/bin/myagent
If the binary is owned by root but not world-executable, a user-context LaunchAgent will fail silently.
Step 4: Add Logging to the Plist
If the path is correct but something else is failing at startup, you need output capture. Edit the plist to add log paths before reloading:
<key>StandardOutPath</key>
<string>/tmp/myagent.stdout.log</string>
<key>StandardErrorPath</key>
<string>/tmp/myagent.stderr.log</string>
Then reload:
launchctl unload ~/Library/LaunchAgents/com.example.myagent.plist
launchctl load ~/Library/LaunchAgents/com.example.myagent.plist
Check the logs immediately:
tail -f /tmp/myagent.stderr.log
Step 5: Consult the Unified Log
For macOS 10.12 and later, launchd emits structured log events to the Unified Logging system:
log show --predicate 'subsystem == "com.apple.launchd"' \
--info --last 5m | grep myagent
Or stream in real time while reloading:
log stream --predicate 'subsystem == "com.apple.launchd"' \
--info | grep myagent
Look for entries containing posix_spawn failures or path does not exist. These are definitive.
Fixing the Plist
Once you’ve identified the correct path, update the plist with PlistBuddy rather than editing by hand to avoid encoding errors:
/usr/libexec/PlistBuddy -c \
"Set :ProgramArguments:0 /opt/homebrew/bin/myagent" \
~/Library/LaunchAgents/com.example.myagent.plist
Verify the change:
/usr/libexec/PlistBuddy -c "Print :ProgramArguments" \
~/Library/LaunchAgents/com.example.myagent.plist
Then reload:
launchctl unload ~/Library/LaunchAgents/com.example.myagent.plist
launchctl load ~/Library/LaunchAgents/com.example.myagent.plist
launchctl list | grep com.example.myagent
A clean load shows a 0 exit code and a PID in the first column.
Preventing Recurrence
The most reliable preventive pattern is using an installer script that resolves the binary path dynamically at install time rather than hardcoding it:
AGENT_BIN=$(which myagent)
sed -i '' "s|__AGENT_PATH__|${AGENT_BIN}|g" \
com.example.myagent.plist.template > \
~/Library/LaunchAgents/com.example.myagent.plist
For agents deployed across mixed Intel and Apple Silicon fleets, always parameterize the binary path at installation rather than baking architecture-specific assumptions into a static plist.
Write a comment