Zum Inhalt springen
SONOYA.com
Apple Mac

Hintergrund-Jobs am Mac mit launchd statt cron

Skripte am Mac per Zeitplan oder beim Anstecken eines Laufwerks starten: LaunchAgent mit launchd anlegen, testen und Fehler finden.

Von

Uhr-Symbol auf dunkelgrauem Verlauf für zeitgesteuerte Hintergrund-Jobs am Mac mit launchd und LaunchAgents statt cron

Auf einem Linux-Server ist cron der Standard für wiederkehrende Aufgaben, und auch auf dem Mac gibt es noch einen Befehl crontab. Trotzdem ist cron am Mac die schlechtere Wahl. Der wichtigste Grund: Ein Mac schläft oft. Steht ein Cronjob auf 3 Uhr nachts und der MacBook-Deckel ist zu, fällt der Lauf einfach aus. Außerdem hat Apple cron nie richtig in die Datenschutzfunktionen von macOS eingebunden, sodass Skripte an geschützten Ordnern scheitern.

macOS bringt mit launchd ein eigenes System für Hintergrundaufgaben mit. Jeder Dienst auf dem Mac läuft darüber, und Du kannst eigene Aufgaben genauso anlegen. launchd holt verpasste Zeitpläne nach dem Aufwachen nach, startet Skripte auch beim Anstecken eines Laufwerks oder wenn sich eine Datei ändert, und schreibt die Ausgabe auf Wunsch in eine Logdatei.

Diese Anleitung zeigt am Beispiel eines Backup-Skripts, wie Du einen sogenannten LaunchAgent anlegst, lädst, testest und Fehler findest. Getestet unter macOS 27.2, die Befehle gelten aber unverändert seit macOS 11.

LaunchAgent oder LaunchDaemon

launchd kennt zwei Arten von Jobs:

  • Ein LaunchAgent läuft als Dein Benutzer, nur solange Du angemeldet bist. Er liegt in ~/Library/LaunchAgents. Das ist für fast alle persönlichen Aufgaben die richtige Wahl.
  • Ein LaunchDaemon läuft als root, unabhängig davon, ob jemand angemeldet ist. Er liegt in /Library/LaunchDaemons. Den brauchst Du nur für Aufgaben, die Administratorrechte benötigen.

Diese Anleitung nutzt einen LaunchAgent.

Schritt 1: Das Skript

Als Beispiel dient ein kleines Skript, das jeden Morgen etwas erledigt. Lege einen Ordner für Deine Skripte an und öffne eine neue Datei:

Code
mkdir -p ~/bin
nano ~/bin/morgen.sh

Füge diesen Inhalt ein:

Code
#!/bin/zsh
echo "Start: $(date)"
/opt/homebrew/bin/restic backup ~/Documents
echo "Fertig: $(date)"

Das Beispiel ruft restic auf, ein Programm für verschlüsselte Backups. Wie Du es einrichtest, steht in Backups mit restic: verschlüsselt auf S3 und Spaces. Du kannst aber jeden beliebigen Befehl einsetzen. Wichtig ist der vollständige Pfad /opt/homebrew/bin/restic. Dazu gleich mehr. Speichere mit Strg + O, bestätige mit Enter, schließe mit Strg + X und mach das Skript ausführbar:

Code
chmod +x ~/bin/morgen.sh

Schritt 2: Die plist-Datei

Ein LaunchAgent wird durch eine Konfigurationsdatei im plist-Format beschrieben. Ihr Name folgt einer Konvention in umgekehrter Domain-Schreibweise, zum Beispiel de.deinname.morgen.plist. Leg die Datei an:

Code
mkdir -p ~/Library/LaunchAgents
nano ~/Library/LaunchAgents/de.deinname.morgen.plist

Füge diesen Inhalt ein und ersetze deinname durch Deinen Benutzernamen am Mac, den Dir whoami im Terminal anzeigt:

Code
<?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>de.deinname.morgen</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/zsh</string>
        <string>/Users/deinname/bin/morgen.sh</string>
    </array>
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>7</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
    <key>StandardOutPath</key>
    <string>/Users/deinname/Library/Logs/morgen.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/deinname/Library/Logs/morgen.log</string>
</dict>
</plist>

Was die Schlüssel bedeuten:

  • Label ist der eindeutige Name des Jobs. Er muss zum Dateinamen ohne .plist passen.
  • ProgramArguments ist der Befehl, aufgeteilt in einzelne Teile. Pfade müssen vollständig sein, eine Tilde wie ~/bin versteht launchd hier nicht.
  • StartCalendarInterval legt die Uhrzeit fest, hier täglich um 7:00 Uhr. Mit zusätzlichen Schlüsseln wie Weekday (0 ist Sonntag) oder Day schränkst Du weiter ein.
  • StandardOutPath und StandardErrorPath leiten alle Ausgaben in eine Logdatei. Ohne diese Zeilen verschwinden Fehlermeldungen spurlos.

Prüfe die Datei auf Tippfehler:

Code
plutil -lint ~/Library/LaunchAgents/de.deinname.morgen.plist

Die Antwort muss mit OK enden. Meldet plutil einen Fehler, stimmt meist ein schließendes Tag nicht.

Schritt 3: Den Job laden und testen

Melde den Job bei launchd an:

Code
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/de.deinname.morgen.plist

gui/$(id -u) bezeichnet Deine Benutzersitzung. id -u liefert dabei Deine Benutzernummer. macOS zeigt danach meist eine Mitteilung, dass ein Hintergrundobjekt hinzugefügt wurde. Das ist eine Schutzfunktion: Alle Hintergrundaufgaben erscheinen in Systemeinstellungen > Allgemein > Anmeldeobjekte & Erweiterungen, und dort lassen sie sich auch abschalten. Ist der Eintrag dort ausgeschaltet, läuft Dein Job nicht.

Statt bis morgen um 7 Uhr zu warten, startest Du den Job sofort:

Code
launchctl kickstart gui/$(id -u)/de.deinname.morgen

Schau dann ins Log:

Code
cat ~/Library/Logs/morgen.log

Dort müssen die Zeilen mit „Start“ und „Fertig“ stehen. Den Zustand des Jobs, inklusive des Exit-Codes des letzten Laufs, zeigt Dir dieser Befehl:

Code
launchctl print gui/$(id -u)/de.deinname.morgen

Such in der Ausgabe nach last exit code. Eine 0 bedeutet Erfolg, jede andere Zahl einen Fehler im Skript.

Was im Ruhezustand passiert

Das ist der größte Vorteil gegenüber cron. Laut Apples Handbuchseite zu launchd.plist startet launchd einen Job mit StartCalendarInterval, der in den Ruhezustand gefallen ist, beim nächsten Aufwachen. Waren es mehrere verpasste Termine, läuft er einmal. Klappst Du das MacBook also um 8 Uhr auf, läuft der Job für 7 Uhr direkt danach.

Zwei Einschränkungen: Ist der Mac ausgeschaltet statt im Ruhezustand, entfällt der Lauf. Und für den Schlüssel StartInterval, der einen Job alle paar Sekunden startet, gilt das Nachholen nicht.

Weitere Auslöser

Statt einer Uhrzeit kannst Du launchd auch auf andere Ereignisse reagieren lassen. Ersetze dafür den Block StartCalendarInterval durch einen dieser Schlüssel:

SchlüsselWirkung
StartInterval mit <integer>3600</integer>alle 3600 Sekunden, also stündlich
StartOnMount mit <true/>jedes Mal, wenn ein Laufwerk eingehängt wird, etwa eine externe SSD
WatchPaths mit einem <array> aus Pfadenwenn sich eine der Dateien oder Ordner ändert, etwa der Download-Ordner
RunAtLoad mit <true/>einmal beim Laden, also auch bei jeder Anmeldung

Mit StartOnMount baust Du zum Beispiel ein Skript, das automatisch Fotos von einer SD-Karte kopiert, sobald Du sie einsteckst. Das Skript prüft dann selbst, ob die richtige Karte unter /Volumes hängt.

Änderungen übernehmen und Job entfernen

launchd liest die plist-Datei nur beim Laden. Nach jeder Änderung meldest Du den Job ab und wieder an:

Code
launchctl bootout gui/$(id -u)/de.deinname.morgen
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/de.deinname.morgen.plist

Willst Du den Job ganz loswerden, führst Du nur den ersten Befehl aus und löschst danach die plist-Datei.

Wenn es nicht klappt

Im Log steht command not found. launchd startet Jobs mit einem sehr knappen Suchpfad, der nur die Systemordner enthält. Programme aus Homebrew unter /opt/homebrew/bin findet es nicht. Schreib sie mit vollem Pfad ins Skript, wie oben bei restic. Den Pfad eines Programms zeigt Dir which programmname im Terminal.

Im Log steht Operation not permitted. Das Skript greift auf einen geschützten Ordner zu, etwa Dokumente, Schreibtisch oder ein externes Laufwerk, und macOS blockiert das. Füge unter Systemeinstellungen > Datenschutz & Sicherheit > Festplattenvollzugriff das Programm hinzu, das in ProgramArguments an erster Stelle steht, hier /bin/zsh. Im Auswahldialog öffnest Du mit Cmd + Umschalt + G ein Feld, in das Du den Pfad direkt eintippen kannst. Bedenke, dass diese Freigabe dann für alle zsh-Skripte gilt, die im Hintergrund laufen.

bootstrap meldet Input/output error. Der Job ist meist schon geladen. Melde ihn mit bootout ab und lade ihn neu. Bleibt der Fehler, prüfe die Datei mit plutil -lint und schau, ob Label zum Dateinamen passt.

Der Job läuft nie, auch nicht mit kickstart. Prüfe unter Systemeinstellungen > Allgemein > Anmeldeobjekte & Erweiterungen, ob der Eintrag erlaubt ist.

Fazit

launchd wirkt mit seinen XML-Dateien sperriger als eine Zeile in der Crontab, ist am Mac aber in allen Punkten überlegen: verpasste Termine werden nach dem Aufwachen nachgeholt, es gibt Auslöser für Laufwerke und Dateiänderungen, und jeder Job ist in den Systemeinstellungen sichtbar. Wenn Du Deinen Mac ohnehin gerade als Arbeitsgerät einrichtest, passt dazu der Leitfaden Mac für Entwickler einrichten: der Leitfaden für 2026.

Weiterlesen.