LaunchAgent Path Mismatch Debugging
- LaunchAgent Path Mismatch Debugging: When Your Agent Refuses to Start
LaunchAgent Path Mismatch Debugging: When Your Agent Refuses to Start
LaunchAgent path mismatches are one of the most quietly frustrating failure modes on macOS systems running autonomous agents. The daemon loads, launchctl reports no errors, and yet your process never appears. No crash log, no obvious stderr output — just silence. Understanding why this happens, and how to systematically diagnose it, is essential operational knowledge for anyone managing persistent background services on macOS.
What a Path Mismatch Actually Means
A LaunchAgent plist references an executable via the ProgramArguments key. When launchd attempts to start the job, it resolves that path literally — no $PATH expansion, no shell interpolation, no symlink chasing by default. If the binary at that path doesn’t exist, isn’t executable by the target user, or has been moved since the plist was written, the job silently fails.
The insidious part: launchctl load succeeds regardless. The plist is syntactically valid, so it loads. The failure only manifests at start time, and depending on your ThrottleInterval setting, that failure may be suppressed repeatedly without any visible signal.
Step-by-Step Diagnostic Approach
1. Check the Job’s Last Exit Status
launchctl list | grep com.yourorg.agentname
This outputs three columns: PID, last exit status, and label. A PID of - with an exit code of 78 is a classic path mismatch indicator. Exit code 78 maps to EX_CONFIG — the process configuration is invalid. Exit code 2 often means the binary was found but exited immediately due to an argument error.
- 78 com.yourorg.agentname
If you see this, you have a configuration-level failure before your process even touched userspace.
2. Inspect the Plist Directly
cat ~/Library/LaunchAgents/com.yourorg.agentname.plist
Locate the ProgramArguments array and extract the first element — that’s your executable path. Then verify it manually:
ls -la /usr/local/bin/your-agent-binary
file /usr/local/bin/your-agent-binary
Check for these specific failure conditions:
- File doesn’t exist: The binary was installed to a different prefix (
/opt/homebrew/binon Apple Silicon vs/usr/local/binon Intel) - File exists but isn’t executable: Missing execute bit for the launchd user context
- Symlink target is broken: The plist points to a symlink whose target was deleted during an upgrade
3. Validate Permissions in the Correct User Context
LaunchAgents run as the logged-in user, but the permission check matters at load time under launchd’s context. Verify:
stat -f "%Sp %Su %Sg" /usr/local/bin/your-agent-binary
# Expected output: -rwxr-xr-x root wheel
A binary owned by root with permissions 700 will fail for a non-root user even if it exists at the correct path. The execute bit must be set for the appropriate group or world.
4. Use log stream to Catch launchd Messages
The most authoritative real-time signal comes from the unified log:
log stream --predicate 'subsystem == "com.apple.launchd"' --level debug 2>&1 | grep -i "your-agent-name"
Then in a separate terminal, trigger the job:
launchctl kickstart -k gui/$(id -u)/com.yourorg.agentname
You’ll often see explicit messages like:
posix_spawnp("/usr/local/bin/your-agent-binary", ...): No such file or directory
This confirms a path mismatch definitively and gives you the exact path launchd tried to resolve.
5. Check for Architecture Mismatch on Apple Silicon
On M-series Macs, a binary compiled only for x86_64 running under Rosetta may resolve differently than a native arm64 binary. If your plist was written for an Intel installation and the binary path changed during an Apple Silicon migration:
lipo -info /usr/local/bin/your-agent-binary
# Universal binary: /usr/local/bin/your-agent-binary is architecture: x86_64 arm64
If the binary is x86_64-only and your Homebrew prefix is now /opt/homebrew, the old plist path /usr/local/bin/... is simply wrong.
Common Fix Patterns
Homebrew prefix migration: Update the plist to reference /opt/homebrew/bin/ instead of /usr/local/bin/. Use brew --prefix to get the canonical path for the current environment.
Post-upgrade broken symlinks: After major version upgrades, versioned binaries sometimes move. Reinstall the package, then reload:
launchctl bootout gui/$(id -u)/com.yourorg.agentname
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.yourorg.agentname.plist
Note: load/unload are deprecated in favor of bootstrap/bootout on macOS Ventura and later.
Hardcoded paths in third-party plists: Some software writes absolute paths at install time. If you move the application or change the install prefix, you must edit the plist manually and reload.
Preventing Future Mismatches
Add a pre-flight validation step to any deployment script that installs LaunchAgent plists:
BINARY_PATH=$(/usr/libexec/PlistBuddy -c "Print :ProgramArguments:0" \
~/Library/LaunchAgents/com.yourorg.agentname.plist)
if [[ ! -x "$BINARY_PATH" ]]; then
echo "ERROR: Binary not found or not executable at $BINARY_PATH"
exit 1
fi
This catches mismatches before the plist is loaded rather than after silent failure.
Write a comment