LaunchAgent Path Mismatch Debugging

LaunchAgent Path Mismatch Debugging: When Your Agent Refuses to Load

LaunchAgents are one of macOS’s most reliable persistence mechanisms—until they aren’t. A misconfigured path is one of the most common and frustrating failure modes: the agent silently fails to load, your service never starts, and launchctl gives you just enough information to be confused but not enough to immediately fix the problem. This post walks through the specific diagnostic workflow for identifying and resolving path mismatch errors in LaunchAgent plists.

Understanding What a Path Mismatch Actually Is

A path mismatch occurs when the executable path declared in your LaunchAgent plist does not match what actually exists on disk. This sounds trivial, but it surfaces in several non-obvious ways:

  • The binary was installed to /usr/local/bin/ but the plist references /opt/homebrew/bin/ (common on Apple Silicon Macs where Homebrew changed its default prefix)
  • A symlink exists but the resolved target has moved or been deleted
  • The executable path is correct but a working directory (WorkingDirectory) key points to a nonexistent location
  • Case sensitivity issues on case-insensitive HFS+ volumes masking the real path

The insidious part is that launchctl load will often succeed without complaint, while the agent itself immediately exits with an error code that only appears in logs.

First-Pass Diagnostics

Start with the most direct check: verify the agent is actually loaded and examine its last exit status.

# List all user LaunchAgents and filter for yours
launchctl list | grep com.yourorg.yourservice

# Output format: PID  ExitCode  Label
# Example of a failing agent:
# -    78       com.yourorg.yourservice

A - in the PID column means the agent is not currently running. The exit code 78 on macOS corresponds to EX_CONFIG, which frequently indicates a configuration or path error.

For a more detailed view, use launchctl print:

launchctl print gui/$(id -u)/com.yourorg.yourservice

Look specifically for the state, last exit code, and path fields in the output. If state shows waiting or the exit code is nonzero immediately after load, you have a runtime failure rather than a load failure.

Tracing the Actual Path Problem

Pull the relevant log entries using log show to see the exact error:

log show --predicate 'subsystem == "com.apple.launchd"' \
  --last 5m \
  --info \
  | grep -i "yourservice"

A path mismatch typically produces entries like:

launchd: (com.yourorg.yourservice) posix_spawnp("/usr/local/bin/myagent", ...): No such file or directory

or:

launchd: (com.yourorg.yourservice) execve failed: 2

Error code 2 is ENOENT—the file does not exist at the specified path. Now you have a concrete target.

Cross-reference the path in your plist against the filesystem:

# Read the declared path from the plist
/usr/libexec/PlistBuddy -c "Print :ProgramArguments:0" \
  ~/Library/LaunchAgents/com.yourorg.yourservice.plist

# Verify it exists and is executable
ls -la /usr/local/bin/myagent

# Check if the path resolves through symlinks
readlink -f /usr/local/bin/myagent

On Apple Silicon Macs, Homebrew binaries live under /opt/homebrew/bin/ rather than /usr/local/bin/. If the binary was installed via Homebrew and the plist was written for an Intel Mac, this is almost certainly your problem.

Fixing the Plist

Once you have the correct path, update the plist directly:

/usr/libexec/PlistBuddy -c \
  "Set :ProgramArguments:0 /opt/homebrew/bin/myagent" \
  ~/Library/LaunchAgents/com.yourorg.yourservice.plist

If you use a Program key instead of ProgramArguments:

/usr/libexec/PlistBuddy -c \
  "Set :Program /opt/homebrew/bin/myagent" \
  ~/Library/LaunchAgents/com.yourorg.yourservice.plist

After editing, unload and reload the agent:

launchctl unload ~/Library/LaunchAgents/com.yourorg.yourservice.plist
launchctl load ~/Library/LaunchAgents/com.yourorg.yourservice.plist
launchctl list | grep com.yourorg.yourservice

A non-- PID in the first column confirms the agent is running.

The WorkingDirectory Trap

A less obvious path mismatch involves the WorkingDirectory key. Even if your binary path is correct, if WorkingDirectory points to a directory that does not exist, the agent will fail with ENOENT or EX_CONFIG. Verify it explicitly:

WORKDIR=$(/usr/libexec/PlistBuddy -c \
  "Print :WorkingDirectory" \
  ~/Library/LaunchAgents/com.yourorg.yourservice.plist 2>/dev/null)

[ -d "$WORKDIR" ] && echo "Exists" || echo "MISSING: $WORKDIR"

If the directory is missing, either create it or remove the WorkingDirectory key from the plist entirely and let the agent default to /.

Validating Before Reload

Before reloading an edited plist, validate its XML structure to avoid a separate category of parse errors:

plutil -lint ~/Library/LaunchAgents/com.yourorg.yourservice.plist

A clean plist returns OK. Any structural error is reported with a line number, which you can then fix before attempting to load.

Systematic Prevention

For any LaunchAgent you ship or manage, embed an explicit path validation step in your installation logic:

BINARY_PATH=$(which myagent 2>/dev/null || echo "")
if [ -z "$BINARY_PATH" ]; then
  echo "ERROR: myagent not found in PATH" >&2
  exit 1
fi
# Write the resolved path into the plist at install time
/usr/libexec/PlistBuddy -c "Set :ProgramArguments:0 $BINARY_PATH" \
  /path/to/com.yourorg.yourservice.plist

This ensures that whatever architecture and Homebrew prefix the host uses, the plist always contains the real path at the moment of installation rather than a hardcoded assumption.

References

Write a comment