LaunchAgent Path Mismatch Debugging

LaunchAgent Path Mismatch Debugging: Finding the Gap Between What macOS Expects and What Exists

When an autonomous agent fails to start on macOS, the first instinct is often to check logs or restart the service. But a surprisingly common and frustrating failure mode sits one layer earlier: the LaunchAgent plist points to a binary or working directory that simply doesn’t exist at the path specified. No crash, no traceback — just silence. Understanding how to systematically diagnose path mismatches saves significant time when managing OpenClaw infrastructure across multiple machines or after system upgrades.

What a Path Mismatch Actually Looks Like

A LaunchAgent plist for an OpenClaw agent might look like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.moeops.openclaw.agent</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/openclaw</string>
    <string>--config</string>
    <string>/etc/openclaw/agent.yaml</string>
  </array>
  <key>WorkingDirectory</key>
  <string>/var/openclaw</string>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
</dict>
</plist>

The agent loads cleanly with launchctl load, returns no error, and then does absolutely nothing. The reason is almost always one of three things: the binary path is wrong, the config path is wrong, or the working directory doesn’t exist. launchd will not tell you which.

The Diagnostic Sequence

Start with launchctl list and filter for your agent label:

launchctl list | grep com.moeops.openclaw

A healthy agent shows a PID in the first column. A path mismatch typically shows a - for PID and an exit code of 78 in the third column:

-   78  com.moeops.openclaw.agent

Exit code 78 is EX_CONFIG — the system-level signal that something about the job’s configuration is invalid. This is your first concrete confirmation you’re dealing with a path or configuration problem, not a runtime error.

Next, inspect the actual plist that launchd has loaded (not just the file on disk):

launchctl print gui/$(id -u)/com.moeops.openclaw.agent

This outputs the live job properties as launchd sees them, including resolved paths. Compare ProgramArguments here against what you expect. On Apple Silicon Macs or after Homebrew migrations, /usr/local/bin may have moved to /opt/homebrew/bin, which is a frequent source of stale paths in older plists.

Then manually verify every path in the plist:

# Check the binary
ls -la /usr/local/bin/openclaw

# Check the config file
ls -la /etc/openclaw/agent.yaml

# Check the working directory
ls -la /var/openclaw

If any of these returns No such file or directory, you’ve found your culprit. For the binary specifically, also verify it’s executable:

test -x /usr/local/bin/openclaw && echo "executable" || echo "not executable"

Tracing with launchd Logging

When the paths look correct on inspection but the agent still won’t start, enable verbose logging for the job:

launchctl log level debug

Then unload and reload the agent:

launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.moeops.openclaw.agent.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.moeops.openclaw.agent.plist

Check the system log stream filtered to launchd:

log stream --predicate 'subsystem == "com.apple.launchd"' --level debug | grep openclaw

This surfaces messages like posix_spawn failures and sandbox denials that launchctl list hides. A true path mismatch produces output similar to:

error: 19: posix_spawn() failed: No such file or directory

That message confirms launchd attempted the spawn and the OS rejected it at the filesystem level.

Sandbox and SIP Interactions

On modern macOS, System Integrity Protection and the sandbox can make path problems look like permission problems. If your binary exists but lives under a path that the user’s sandbox profile doesn’t allow execution from, you’ll see the same 78 exit code. Validate this by temporarily testing the command manually as the same user context the agent runs under:

sudo -u $(whoami) /usr/local/bin/openclaw --config /etc/openclaw/agent.yaml

If that succeeds interactively but fails via launchd, examine whether StandardOutPath and StandardErrorPath in the plist point to writable locations. An unwritable log path causes launchd to abort the job before the binary even executes, masking the real problem.

Hardening Against Future Mismatches

Once you’ve corrected the paths, add a validation step to your deployment tooling. A simple shell check before any launchctl bootstrap call catches problems at install time:

validate_plist_paths() {
  local binary
  binary=$(defaults read "$1" ProgramArguments | grep -m1 '/')
  if [[ ! -x "$binary" ]]; then
    echo "ERROR: Binary not found or not executable: $binary"
    return 1
  fi
}

Also consider pinning the WorkingDirectory to a path you control explicitly, and use StandardOutPath and StandardErrorPath so any early startup output is captured rather than lost to the void. Path discipline at plist authoring time is significantly cheaper than debugging silence at 2am.

References

Write a comment