LaunchAgent Path Mismatch Debugging

LaunchAgent Path Mismatch Debugging: When Your Agent Refuses to Start

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 most frustrating failure modes, often producing no useful error output by default. This post walks through how path mismatches occur, how to diagnose them systematically, and how to resolve them without guessing.

Understanding the Failure Mode

When launchd attempts to load a LaunchAgent, it reads the ProgramArguments key from the plist and tries to execute the specified binary. If the path doesn’t resolve to a real, executable file, launchd logs an error and the job either fails to load or loads but immediately exits. The problem is that launchctl often reports a misleading status rather than a clear “file not found” message.

A typical broken plist might look like this:

<key>ProgramArguments</key>
<array>
    <string>/usr/local/bin/myagent</string>
    <string>--config</string>
    <string>/etc/myagent/config.yaml</string>
</array>

If the binary was installed to /opt/homebrew/bin/myagent (common on Apple Silicon Macs) or moved during an upgrade, the agent silently fails on every boot.

Diagnosing the Problem

Start with launchctl list to see the job’s exit code:

launchctl list | grep myagent

Output like this is a red flag:

-       78      com.example.myagent

The 78 exit code corresponds to EX_CONFIG, a strong signal that something in the plist configuration is wrong before the binary even runs. Exit code 2 or a dash in the PID column with a non-zero exit status are also common indicators.

Next, inspect the system log directly. Use log show with a time window around the last boot or the last time you attempted to load the agent:

log show --predicate 'subsystem == "com.apple.launchd"' --last 5m | grep myagent

You’re looking for messages like:

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

or

Could not find and/or execute program specified for job: com.example.myagent

These messages confirm the binary path is the problem, not the plist syntax or permissions.

Verifying the Binary Path

Once you suspect a path mismatch, confirm where the binary actually lives:

which myagent
# or
command -v myagent

# Check common alternate locations
ls -la /usr/local/bin/myagent
ls -la /opt/homebrew/bin/myagent
ls -la /usr/bin/myagent

On Apple Silicon machines running Homebrew, binaries live under /opt/homebrew/bin/ rather than /usr/local/bin/. A plist written on an Intel Mac and copied to an M-series Mac without updating the path is a classic source of this bug.

Also verify execute permissions:

ls -la $(which myagent)
# Should show -rwxr-xr-x or similar

A binary that exists but lacks execute permissions for the launching user produces a similar failure mode.

Validating the Plist Structure

Before reloading, validate the plist itself:

plutil -lint ~/Library/LaunchAgents/com.example.myagent.plist

A valid plist returns OK. An invalid one gives a line-number reference to the syntax error. This doesn’t catch logical errors like wrong paths, but it eliminates malformed XML as a cause.

Also check ownership and permissions on the plist file itself:

ls -la ~/Library/LaunchAgents/com.example.myagent.plist

The plist must be owned by the user (for user-level LaunchAgents) or root (for /Library/LaunchAgents), and it must not be group- or world-writable. If it is, launchd will refuse to load it as a security measure, producing an error like:

Dubious ownership on file (skipping): /Library/LaunchAgents/com.example.myagent.plist

Fix with:

chmod 644 ~/Library/LaunchAgents/com.example.myagent.plist

Correcting the Path and Reloading

Edit the plist to point to the correct binary:

# Find the real path first
AGENT_PATH=$(command -v myagent)
echo $AGENT_PATH

# Edit the plist
nano ~/Library/LaunchAgents/com.example.myagent.plist

Update the ProgramArguments array with the verified path. Then reload the agent properly — unloading before reloading avoids stale job state:

launchctl unload ~/Library/LaunchAgents/com.example.myagent.plist
launchctl load ~/Library/LaunchAgents/com.example.myagent.plist

On macOS Ventura and later, the preferred commands are:

launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.example.myagent.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.myagent.plist

Verify the result:

launchctl list | grep myagent
# Look for a PID in the first column and 0 in the exit code column

Preventing Future Mismatches

Use the Program key alongside ProgramArguments to make the binary path explicit and separate from the argument list. Consider building plist files programmatically during install scripts so the path is always derived at install time rather than hardcoded:

AGENT_BIN=$(command -v myagent)
/usr/libexec/PlistBuddy -c "Set :ProgramArguments:0 $AGENT_BIN" \
    ~/Library/LaunchAgents/com.example.myagent.plist

Running this as part of a post-install hook ensures the plist always reflects the actual installed location, regardless of architecture or Homebrew prefix.

References

Write a comment