LaunchAgent Path Mismatch Debugging

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/bin on Apple Silicon vs /usr/local/bin on 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.

References

Write a comment