"Life is all about sharing. If we are good at something, pass it on." - Mary Berry

launchctl: Bootstrap failed: 5: Input/output error

2025-09-26

Categories: Programming

Initial symtoms

We have an internal service that needs to start a boot. The responsible team wrote a simple launchd script, 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.example.watchdog</string>

    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/watchdog</string>
    </array>

    <key>RunAtLoad</key>
    <true/>

    <key>KeepAlive</key>
    <true/>

    <key>SessionCreate</key>
    <true/>

    <key>StandardOutPath</key>
    <string>/var/log/watchdog.out</string>

    <key>StandardErrorPath</key>
    <string>/var/log/watchdog.err</string>
</dict>
</plist>

However, when trying to load it, we ran into this error:

sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.watchdog.plist
Password:
Bootstrap failed: 5: Input/output error

Interestingly, the same error also appears if the service is already running. But in this case, it wasn’t:

sudo launchctl bootout system /Library/LaunchDaemons/com.example.watchdog.plist
Boot-out failed: 5: Input/output error

To rule out issues with the binary itself, I tried starting it manually:

/usr/local/bin/watchdog

It ran without problems, so the error cleary happens before the binary is executed.

Unfortunately, man launchctl shows no option to enable debugging for bootstrap, which makes troubleshooting trickier.

Narrowing down the cause

I then tried swapping the binary path in a known working launchd script with /usr/local/bin/watchdog. The modified script looked 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>Crashed</key>
        <true/>
        <key>KeepAlive</key>
        <true/>
        <key>Label</key>
        <string>org.openvpn.client</string>
        <key>ProgramArguments</key>
        <array>
                <string>/usr/local/bin/watchdog</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
        <key>SessionCreate</key>
        <true/>
        <key>StandardOutPath</key>
        <string>/var/log/ovpnagent.log</string>
</dict>
</plist>

By removing keys one by one, I eventually found the culprit: the label value. If the label is set to com.example.watchdog, the bootstrap failed. If I changed it to something else, it worked.

Root cause

It turned out the service had been accidentally disabled during earlier experiments with the .plist. Running:

sudo launchctl print system

revealed this:

disabled services = {
        "com.example.watchdog" => disabled
}

Fix

The solution is to re-enable the service and then bootstrap it again:

sudo launchctl enable system/com.example.watchdog
sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.watchdog.plist

Tags: macOS launchd

Edit on GitHub