launchd - Script Management in macOS

Edit - Oct 24, 2023

After upgrading to macOS Sonoma, my custom agent for automatically syncing some directories on my machine using rsync broke, due to what I believe to be a permissions issue. For this reason, I revisited this topic and learned more about this system in order to see if I could get around these permissions issues. This included adding more notes in general as I read through man pages and other articles.

Further, the original authoring of this document usually referred to the legacy use of launchctl, sometimes called launchctl 1. The updates here try to incorporate “launchctl 2” subcommands and usage.

What is it?

What is a Daemon?

A daemon is a program running in the background without requiring user input.

What is an Agent?

How to use it?

plist Locations Overview

launchctl Subcommand Domains, Services, and Endpoints

Many subcommands in launchctl take a specifier which indicates the target domain or service for the subcommand

Terminology

Target Domains

Important Subcommands

Note: There are a number of subcommands listed in documentation that are now considered “legacy”, like load, unload, start, stop, list, etc… Below is the list of updated subcommands and their uses

Other Important Note: [service-target] (seen below in many commands) takes the form of <domain-target>/<service-id>

How to write a .plist file

The majority of the learning that I needed in order to run my background job came from the first link in the Further Reading section below

Example .plist file

This is an example .plist file that I use to run a background job to sync my Obsidian-based Second Brain to the git-based version of the repo, which then runs some formatting and other processes.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>Label</key>
        <string>dev.johnturner.ObsidianFoamReconciler</string>

        <key>ServiceDescription</key>
        <string>Regular sync between Obsidian Second Brain vault and my git-based Second Brain</string>

        <key>Program</key>
        <string>/Users/johnturner/second-brain/sync-from-obsidian.sh</string>

        <key>WorkingDirectory</key>
        <string>/Users/johnturner/second-brain</string>

        <key>StandardErrorPath</key>
        <string>/Users/johnturner/Desktop/reconciler-error.log</string>

        <key>EnvironmentVariables</key>
        <dict>
            <key>PATH</key>
            <string>
                /usr/local/opt/gettext/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
            </string>
        </dict>

        <key>StartCalendarInterval</key>
        <dict>
            <key>Hour</key>
            <integer>11</integer>
            <key>Minute</key>
            <integer>0</integer>
        </dict>

        <key>RunAtLoad</key>
        <false />
    </dict>
</plist>

Misc. Notes

Further Reading