Codonic

LD Manager guide · launchd

launchd exit code 78 (EX_CONFIG): why your LaunchAgent won’t run

launchd could not start the program as configured, so your program never ran. Where 78 shows up, the four checks that find the cause, and the other exit codes.

Where you see 78

The same failure shows up in three places, with three different-looking numbers:

$ launchctl list | grep com.example.report
-	78	com.example.report

$ launchctl print gui/$(id -u)/com.example.report
	state = spawn scheduled
	runs = 1
	last exit code = 78: EX_CONFIG

$ launchctl list com.example.report
	"LastExitStatus" = 19968;

In launchctl list, the first column is the PID (- means not running) and the second is the last exit status. launchctl print names it: EX_CONFIG, the “configuration error” code from sysexits(3). And launchctl list <label> prints the raw wait status, which is the exit code times 256: 19968 is 78 × 256. That is why one Apple developer forums thread carries both in its title, Error: 78, LastExitStatus: 19968: they are the same failure.

If you ask launchctl itself, you get a fourth answer:

$ launchctl error 78
78: Function not implemented

launchctl error reads 78 as a POSIX error number (ENOSYS), which is a different table. For a job’s exit status, ignore it: the sysexits meaning, EX_CONFIG, is the one that applies.

Why launchd reports EX_CONFIG

When launchd starts a job, it sets up the process first, including the log files you named, and then executes the program. If that setup or the execution itself fails, launchd records the job as having exited with 78. Nothing reaches your log, because your program never ran: an empty log file, or no log file at all, together with 78 is the giveaway.

runs = 1 and a state of spawn scheduled mean launchd tried once and will try again when the job’s next trigger fires (or after the throttle interval, 10 seconds by default, for a KeepAlive job). It will keep failing the same way until the configuration changes.

In the commands below, com.example.report is the job’s Label and its file is ~/Library/LaunchAgents/com.example.report.plist. Use your own.

Check 1: the program path

Read the path launchd will execute: Program, or the first item of ProgramArguments.

plutil -p ~/Library/LaunchAgents/com.example.report.plist

Then check that file, by its exact path:

ls -l /Users/alice/bin/report.sh
xattr -l /Users/alice/bin/report.sh
head -1 /Users/alice/bin/report.sh
  • It must exist. A plist left behind by an app you removed, or a script you moved, points at nothing. This is the most common cause.
  • It must be executable. -rwxr-xr-x has the x; if not, chmod +x it.
  • No ~. launchd does not expand ~ or $HOME in Program or ProgramArguments. Write /Users/alice/bin/report.sh.
  • No quarantine. A com.apple.quarantine attribute from a download can stop the program from running. If you trust the file, xattr -d com.apple.quarantine /Users/alice/bin/report.sh.
  • A script needs a valid first line, such as #!/bin/zsh, pointing at an interpreter that exists. Or call the interpreter directly: ProgramArguments = /bin/zsh, /Users/alice/bin/report.sh.

Check 2: the log folders

StandardOutPath and StandardErrorPath name files, and launchd creates the file if it is missing, but not the folder it lives in. A missing folder is the other classic source of 78:

mkdir -p /Users/alice/Library/Logs/report

Three more traps reported with exactly this symptom:

  • the log path contains ~, which launchd does not expand here either;
  • the log folder is on another disk that is not mounted when the job starts;
  • the log path is inside a privacy-protected folder such as Desktop, Documents or Downloads. Keep launchd’s own logs in ~/Library/Logs (written out in full) or /tmp.

Check 3: the working directory and the user

Less often, the setup fails on other keys. If the plist has WorkingDirectory, that folder must exist; a LaunchDaemon with UserName must name an account that exists on this Mac. Both are easy to miss when you copy a plist from another computer.

Check 4: it was your program

Some programs exit with 78 themselves, following the same sysexits convention, when their own configuration file is wrong. If the log shows output from your program, or runs is climbing while every run fails fast, the program started, and 78 is its message to you about its own settings. Run the exact command from ProgramArguments in a terminal to see what it says, then compare the environment: launchd gives jobs a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin) and none of your shell’s settings.

Run it again

After you fix the file or folder, ask launchd to start the job now instead of waiting for its schedule:

launchctl kickstart -k gui/$(id -u)/com.example.report
launchctl print gui/$(id -u)/com.example.report | grep -E 'state|last exit'

If you edited the plist itself, launchd is still holding the old copy: unload and load it first.

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

For a LaunchDaemon, use the system domain with sudo. If the bootstrap itself fails, see “Bootstrap failed: 5: Input/output error”.

The other codes you’ll see

Status Meaning First thing to check
0 Exited normally Nothing, unless it should still be running
1 A general failure reported by the program The job’s own log
2 Usually wrong command-line usage The arguments in ProgramArguments
77 EX_NOPERM: permission denied File permissions, or privacy access for the program
78 EX_CONFIG: launchd could not start it as configured This page
126 The program exists but could not be executed Permissions, or a script without a valid first line
127 A shell could not find the command Full paths inside the script; PATH is minimal
-9 Killed by SIGKILL Memory pressure, or something force-quit it
-15 Killed by SIGTERM Something stopped it: a logout, launchctl kill, a restart

Negative numbers are signals, as man launchctl says of list. A number between 129 and 159 is usually a shell reporting that its child was killed by signal number minus 128.

The shortcut: LD Manager

LD Manager is a free, open-source Mac app and command-line tool for launchd jobs. It joins every layer of a job’s state (the plist, whether launchd has it loaded, the enable or disable override, the Login Items switch, the PID, the run count and the last exit code) into one status, and explains the exit code in words. Its Doctor checks each cause on this page: a missing, non-executable or quarantined program, an unexpanded ~, a missing log folder, wrong ownership or permissions, and restart loops. Each finding comes with a fix (choose the program, create the folder, remove the quarantine) and every edit shows a diff and keeps a backup.

ldm doctor com.example.report
ldm explain 78
ldm restart com.example.report --dry-run

ldm explain decodes any status, signals included, and --dry-run prints the exact launchctl commands instead of running them. LD Manager needs macOS 26 or later. It reads the same state the commands above read; when a program fails on its own, the answer is still in that program’s log.

Exit code 78

What does exit code 78 mean for a launchd job?

It is EX_CONFIG, the configuration error code from sysexits(3). For a LaunchAgent or LaunchDaemon it usually means launchd could not start the program as configured: the program is missing, not executable or quarantined, a path contains ~, or the folder for StandardOutPath or StandardErrorPath does not exist.

Why does launchctl error 78 say “Function not implemented”?

launchctl error reads 78 as a POSIX error number, ENOSYS, which is a different table. For a job’s exit status the sysexits meaning applies, which is why launchctl print shows “last exit code = 78: EX_CONFIG”.

What is LastExitStatus 19968?

launchctl list <label> prints the raw wait status, which is the exit code times 256. 19968 is 78 × 256, the same EX_CONFIG failure that launchctl list shows as 78 in its Status column.

Why is my LaunchAgent’s log file empty?

With exit code 78, the program usually never ran, so it wrote nothing. Check that the program path exists and is executable, and that the log folder exists: launchd creates the log file but not its folder, and does not expand ~ in the path.

What does a negative exit code like -9 or -15 mean?

A negative number in launchctl list is the signal that stopped the job: -15 is SIGTERM, something asked it to quit, and -9 is SIGKILL, often memory pressure or a force quit.