LaunchAgent Path Mismatch Debugging
LaunchAgent Path Mismatch Debugging: When Your Agent Refuses to Load
LaunchAgents are one of macOS’s most reliable mechanisms for running persistent background processes — until they aren’t. One of the most frustrating failure modes is the path mismatch, where a LaunchAgent plist is syntactically valid, references an executable that exists on disk, and still silently fails to load or run. This post walks through the diagnostic methodology for tracking down path mismatches, explains why they occur, and provides the specific commands you need to resolve them efficiently.
Understanding What “Path Mismatch” Actually Means
A path mismatch in LaunchAgent context isn’t always a simple “file not found” error. It encompasses several distinct failure categories:
- The
ProgramArgumentskey references an absolute path that resolves differently under the launch context (e.g., symlinks that don’t follow in restricted environments) - The executable exists but is not accessible to the user context under which
launchdis loading the agent - The working directory (
WorkingDirectorykey) doesn’t exist or is inaccessible at load time - A wrapper script references binaries using relative paths or
$PATHexpansion that launchd does not honor
The critical thing to understand is that launchd does not inherit a normal shell environment. There is no $PATH, no ~ expansion, and no sourced profile. Every path in your plist must be fully qualified and resolvable without environment expansion.
Initial Triage: Loading State and Error Codes
Start with launchctl to check whether the agent is even registered:
launchctl list | grep com.yourorg.youragent
The output columns are: PID, last exit status, and label. A - in the PID column means the agent is not currently running. An exit status of 78 is particularly relevant — it maps to EX_CONFIG, indicating a configuration error that often originates from a bad path.
To get more detail on a specific agent:
launchctl list com.yourorg.youragent
This returns a dictionary including LastExitStatus. If you see a non-zero value, convert it:
# Exit status 256 means the underlying exit code was 1
# Exit status 32512 often means "executable not found" (errno 127 from the shell)
python3 -c "import os; print(os.strerror(72))"
For agents that fail before logging begins, check the system log:
log show --predicate 'subsystem == "com.apple.launchd"' --last 10m --info
Or more targeted:
log show --predicate 'process == "launchd" AND eventMessage CONTAINS "youragent"' --last 30m
Verifying the Executable Path
The most direct diagnostic is validating what launchd will actually see. Pull the ProgramArguments value from the plist:
/usr/libexec/PlistBuddy -c "Print :ProgramArguments" \
~/Library/LaunchAgents/com.yourorg.youragent.plist
Take the first element (the executable) and run a full verification:
EXEC_PATH="/usr/local/bin/myagent"
# Does it exist?
test -e "$EXEC_PATH" && echo "exists" || echo "missing"
# Is it executable?
test -x "$EXEC_PATH" && echo "executable" || echo "not executable"
# Is it a symlink, and where does it actually resolve?
readlink -f "$EXEC_PATH"
# What user will launchd run this as?
stat -f "%Su %Sg %Sp" "$EXEC_PATH"
A common trap: /usr/local/bin/myagent is a symlink to a Homebrew-managed binary inside /opt/homebrew/... on Apple Silicon. The symlink resolves fine in a terminal session but may traverse into a path that carries different permissions or doesn’t exist if Homebrew updated the package and rotated the binary path.
The Sandbox and User Context Problem
Even if the path resolves correctly, the agent may fail because launchd loads agents in a constrained context. Check if the plist itself has a UserName or GroupName key that differs from the file’s owner:
/usr/libexec/PlistBuddy -c "Print :UserName" \
~/Library/LaunchAgents/com.yourorg.youragent.plist
If the plist specifies a different user than the logged-in session owner, the agent won’t load from ~/Library/LaunchAgents at all — that directory is exclusively for the GUI session user. System-level agents that must run as another user belong in /Library/LaunchDaemons/, not LaunchAgents.
Validate plist syntax and key correctness with:
plutil -lint ~/Library/LaunchAgents/com.yourorg.youragent.plist
And do a full schema check with:
plutil -p ~/Library/LaunchAgents/com.yourorg.youragent.plist
Forcing a Clean Reload
When you’ve corrected a path issue, don’t assume launchctl load on an already-registered agent will pick up your changes. The correct sequence is:
# Unload first (suppress error if not loaded)
launchctl unload ~/Library/LaunchAgents/com.yourorg.youragent.plist 2>/dev/null
# Force load and watch for immediate error output
launchctl load -w ~/Library/LaunchAgents/com.yourorg.youragent.plist
# Confirm it's running
launchctl list com.yourorg.youragent
On macOS Ventura and later, prefer the bootstrap/bootout domain approach:
DOMAIN="gui/$(id -u)"
launchctl bootout $DOMAIN ~/Library/LaunchAgents/com.yourorg.youragent.plist
launchctl bootstrap $DOMAIN ~/Library/LaunchAgents/com.yourorg.youragent.plist
Capturing What the Agent Actually Sees
If the agent loads but misbehaves, add temporary output keys to your plist to capture its runtime environment:
<key>StandardOutPath</key>
<string>/tmp/youragent.stdout.log</string>
<key>StandardErrorPath</key>
<string>/tmp/youragent.stderr.log</string>
Then add a diagnostic line at the top of your wrapper script:
#!/bin/bash
echo "PATH=$PATH" >> /tmp/youragent.env.log
echo "PWD=$(pwd)" >> /tmp/youragent.env.log
echo "USER=$(whoami)" >> /tmp/youragent.env.log
which myexecutable >> /tmp/youragent.env.log 2>&1
This frequently reveals that the agent is running with a minimal PATH like /usr/bin:/bin:/usr/sbin:/sbin — which excludes /usr/local/bin and /opt/homebrew/bin entirely.
Summary
Path mismatch debugging in LaunchAgents demands that you abandon shell assumptions. Launchd is not a shell; it does not expand variables, follow your profile, or inherit your session PATH. Every path must be absolute, every executable must be reachable by the exact user context launchd will use, and every symlink chain must resolve cleanly. When in doubt, instrument the agent’s own startup to log what it actually sees at runtime — that evidence eliminates guesswork faster than any external diagnostic tool.
Write a comment