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 Nathan Base
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:
mkdir -p ~/bin
nano ~/bin/morgen.shFüge diesen Inhalt ein:
#!/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:
chmod +x ~/bin/morgen.shSchritt 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:
mkdir -p ~/Library/LaunchAgents
nano ~/Library/LaunchAgents/de.deinname.morgen.plistFüge diesen Inhalt ein und ersetze deinname durch Deinen Benutzernamen am Mac, den Dir whoami im Terminal anzeigt:
<?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:
Labelist der eindeutige Name des Jobs. Er muss zum Dateinamen ohne.plistpassen.ProgramArgumentsist der Befehl, aufgeteilt in einzelne Teile. Pfade müssen vollständig sein, eine Tilde wie~/binversteht launchd hier nicht.StartCalendarIntervallegt die Uhrzeit fest, hier täglich um 7:00 Uhr. Mit zusätzlichen Schlüsseln wieWeekday(0 ist Sonntag) oderDayschränkst Du weiter ein.StandardOutPathundStandardErrorPathleiten alle Ausgaben in eine Logdatei. Ohne diese Zeilen verschwinden Fehlermeldungen spurlos.
Prüfe die Datei auf Tippfehler:
plutil -lint ~/Library/LaunchAgents/de.deinname.morgen.plistDie 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:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/de.deinname.morgen.plistgui/$(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:
launchctl kickstart gui/$(id -u)/de.deinname.morgenSchau dann ins Log:
cat ~/Library/Logs/morgen.logDort müssen die Zeilen mit „Start“ und „Fertig“ stehen. Den Zustand des Jobs, inklusive des Exit-Codes des letzten Laufs, zeigt Dir dieser Befehl:
launchctl print gui/$(id -u)/de.deinname.morgenSuch 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üssel | Wirkung |
|---|---|
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 Pfaden | wenn 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:
launchctl bootout gui/$(id -u)/de.deinname.morgen
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/de.deinname.morgen.plistWillst 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.
-
Apple MacMac für Entwickler einrichten: der Leitfaden für 2026
Neuen Mac fürs Programmieren einrichten: Homebrew, Git, SSH, GitHub, VS Code, Python, Docker, Claude Code und ein Brewfile für den nächsten Mac.
Nathan Base -
Raspberry Pi & LinuxAnleitung: Cronjobs unter Raspberry Pi OS einrichten
Cronjobs unter Raspberry Pi OS Trixie einrichten: Zeitzone setzen, crontab verstehen, Bash- und Python-Scripts samt venv starten und Fehler protokollieren.
Nathan Base -
Web & CodeBackups mit restic: verschlüsselt auf S3 und Spaces
Server und Mac mit restic sichern: verschlüsselt, inkrementell und automatisch auf DigitalOcean Spaces oder anderen S3-Speicher, samt Restore.
Nathan Base -
Apple MacMac: Systemdaten riesig? Was dahinter steckt
Systemdaten belegen am Mac 80 GB und mehr? So findest Du heraus, was dahinter steckt, von Time-Machine-Snapshots bis Docker, und was weg darf.
Nathan Base -
Apple MacSpotlight findet nichts mehr: Index am Mac neu aufbauen
Spotlight findet keine Dateien oder Apps mehr, oder der Index-Prozess frisst CPU? So prüfst Du den Index unter macOS 27 und baust ihn neu auf.
Nathan Base -
Apple MacmacOS 27: Kurzbefehle per Beschreibung erstellen
Mit „Kurzbefehl beschreiben“ baut macOS 27 Automationen aus einem Satz. So nutzt Du die Funktion, mit fünf fertigen Beispielen und den typischen Grenzen.
Nathan Base