devAlice
← AI Agents

KI-Agenten im Hintergrund ausführen — cron · launchd · Task Scheduler

Planungsmuster zum automatischen Ausführen von KI-Agenten wie Claude Code — macOS launchd · Linux cron · Windows Task Scheduler im Vergleich.

KI-Agenten wie Claude Code eignen sich nicht nur für den interaktiven Einsatz am Keyboard. Nächtliche PR-Reviews, Issue-Triage, README-Synchronisierung, Log-Zusammenfassungen — der Mehrwert wächst, wenn sie unbeaufsichtigt laufen.

Ich denke, was autonome Agenten so wertvoll macht, ist nicht die Anzahl der Aufgaben, die sie ausführen, sondern die Zuverlässigkeit, mit der sie es tun — weil ein Agent, der nachts schweigend scheitert, schlimmer ist als keiner. Früher haben wir KI-Tools hauptsächlich interaktiv eingesetzt; heute verlagert sich die Arbeit zunehmend in geplante, unbeaufsichtigte Läufe, vielmehr als Erweiterung des Teams als als Ersatz manueller Arbeit.

Dieser Leitfaden vergleicht die Standard-Planungstools auf drei Betriebssystemen (macOS launchd, Linux cron, Windows Task Scheduler) anhand desselben Szenarios und beleuchtet KI-Agenten-spezifische Fallen (API-Key-Sicherheit, Log-Erfassung, Fehlerbenachrichtigungen).

TL;DR

BetriebssystemToolDefinitionLogs
macOSlaunchd (LaunchAgent)~/Library/LaunchAgents/<label>.plistStandardOutPath/StandardErrorPath
Linuxcroncrontab -e oder /etc/cron.d/<name>MTA oder Umleitung >> log.txt 2>&1
WindowsTask SchedulerGUI oder PowerShell Register-ScheduledTaskEvent Viewer oder Dateiausgabe

Fünf häufige Fallen:

  1. Leerer PATH — Befehle werden nicht gefunden
  2. API-Keys im Klartext in plist/crontab (NICHT empfohlen)
  3. Gleichzeitige Ausführungen desselben Jobs können sich gegenseitig stören
  4. Stilles Versagen — keine Fehlerbenachrichtigung
  5. Ruhender Computer — Job wird vollständig übersprungen

§1–§3 behandeln betriebssystemspezifische Muster; §4 die Fallen.

Voraussetzungen

  • Ein KI-Agent zum Ausführen (z. B. Claude Code, Cursor CLI oder ein eigenes Skript)
  • API-Keys über Umgebungsvariablen oder einen Secret-Manager verwaltet
  • Ein Shell-Skript (z. B. ~/agents/daily-pr-review.sh)

1. macOS — launchd LaunchAgent — 10 Min.

cron ist auf macOS verfügbar, aber Apple empfiehlt launchd. Nachholläufe nach Schlafmodus oder erneuter Anmeldung sind damit zuverlässiger.

1.1 Die plist schreiben

~/Library/LaunchAgents/dev.local.daily-pr-review.plist:

<?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>dev.local.daily-pr-review</string>
 
  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>-lc</string>
    <string>$HOME/agents/daily-pr-review.sh</string>
  </array>
 
  <!-- Umgebung: PATH kann leer sein, daher explizit setzen -->
  <key>EnvironmentVariables</key>
  <dict>
    <key>PATH</key>
    <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
    <!-- ANTHROPIC_API_KEY NICHT hier eintragen — siehe §4.2 -->
  </dict>
 
  <!-- Täglich um 09:00 -->
  <key>StartCalendarInterval</key>
  <dict>
    <key>Hour</key><integer>9</integer>
    <key>Minute</key><integer>0</integer>
  </dict>
 
  <!-- Nachholen nach Schlafmodus -->
  <key>RunAtLoad</key><false/>
  <key>StartOnMount</key><false/>
 
  <!-- Logs -->
  <key>StandardOutPath</key>
  <string>/tmp/daily-pr-review.out.log</string>
  <key>StandardErrorPath</key>
  <string>/tmp/daily-pr-review.err.log</string>
</dict>
</plist>

1.2 Registrieren + Starten

# Registrieren
launchctl load ~/Library/LaunchAgents/dev.local.daily-pr-review.plist
 
# Sofort starten (Zeitplan überspringen)
launchctl start dev.local.daily-pr-review
 
# Status
launchctl list | grep daily-pr-review
 
# Deregistrieren
launchctl unload ~/Library/LaunchAgents/dev.local.daily-pr-review.plist

1.3 Nachholen nach Schlafmodus

cron überspringt Läufe, die in den Schlafmodus fallen. launchd hingegen führt den Job einmal nach dem Aufwachen nach (Hinweis: StartCalendarInterval holt dabei nur einmal nach).

Für stündliches Nachholen:

<key>StartInterval</key>
<integer>3600</integer>  <!-- jede Stunde -->

→ Läuft einmal nach dem Aufwachen, anders als cron.


2. Linux — cron — 5 Min.

2.1 crontab bearbeiten

crontab -e

Beispiel:

# m h dom mon dow  Befehl
0 9 * * *  /home/user/agents/daily-pr-review.sh >> /home/user/logs/pr-review.log 2>&1
FeldWertBedeutung
00zur vollen Stunde
999 Uhr
* * *jederjeden Tag

Täglich 09:00, stdout/stderr an Logdatei anhängen.

2.2 PATH explizit setzen

Der PATH von cron ist stark eingeschränkt. Er sollte im Skript selbst oder am Anfang der crontab gesetzt werden:

PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/sbin
SHELL=/bin/bash
 
0 9 * * *  $HOME/agents/daily-pr-review.sh >> $HOME/logs/pr-review.log 2>&1

2.3 System-cron (/etc/cron.d/)

Für root-Registrierung auf Servern:

sudo tee /etc/cron.d/daily-pr-review > /dev/null <<'EOF'
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
0 9 * * * appuser /home/appuser/agents/daily-pr-review.sh >> /var/log/agent.log 2>&1
EOF

Das Feld appuser gibt den Ausführungsbenutzer an. Wichtig für die Sicherheit.

2.4 systemd Timer (moderne Alternative)

Eine moderne Alternative zu cron. Unit-Dateien vereinen Neustart-Logik, Fehlerbehandlung und zentrale Protokollierung.

# /etc/systemd/system/pr-review.service
[Unit]
Description=Täglicher PR-Review-Agent
 
[Service]
Type=oneshot
User=appuser
ExecStart=/home/appuser/agents/daily-pr-review.sh
StandardOutput=journal
StandardError=journal
# /etc/systemd/system/pr-review.timer
[Unit]
Description=Täglichen PR-Review um 09:00 ausführen
 
[Timer]
OnCalendar=*-*-* 09:00:00
Persistent=true   # nach Boot ausführen, wenn Zeit während Downtime verstrichen
 
[Install]
WantedBy=timers.target
sudo systemctl enable --now pr-review.timer
journalctl -u pr-review.service    # Logs

Persistent=true ist der entscheidende Vorteil gegenüber cron: Der Timer holt verpasste Läufe nach Server-Ausfallzeiten automatisch nach.


3. Windows — Task Scheduler — 10 Min.

Die GUI funktioniert, ist aber nicht reproduzierbar. Die PowerShell-Variante ist die empfohlene Methode.

3.1 Per PowerShell registrieren

# Aktion
$action = New-ScheduledTaskAction `
  -Execute "wsl.exe" `
  -Argument "-d Ubuntu -e bash -lc '~/agents/daily-pr-review.sh'"
 
# Auslöser: täglich 09:00
$trigger = New-ScheduledTaskTrigger -Daily -At 9am
 
# Einstellungen: auch ohne angemeldeten Benutzer ausführen; aus Schlaf aufwecken
$settings = New-ScheduledTaskSettingsSet `
  -StartWhenAvailable `
  -AllowStartIfOnBatteries `
  -DontStopIfGoingOnBatteries `
  -ExecutionTimeLimit (New-TimeSpan -Hours 1) `
  -RestartCount 2 `
  -RestartInterval (New-TimeSpan -Minutes 10)
 
# Registrieren
Register-ScheduledTask `
  -TaskName "DailyPRReview" `
  -Action $action `
  -Trigger $trigger `
  -Settings $settings `
  -Description "Claude Code tägliches PR-Review in WSL ausführen"

3.2 In der GUI bestätigen

Win + Rtaskschd.msc → Task Scheduler-Bibliothek → „DailyPRReview".

3.3 Sofort testen

Start-ScheduledTask -TaskName "DailyPRReview"
Get-ScheduledTaskInfo -TaskName "DailyPRReview" | Select LastRunTime, LastTaskResult

LastTaskResult: 0 bedeutet Erfolg.

3.4 WSL aufrufen

Wie oben gezeigt: wsl.exe -d Ubuntu -e bash -lc '...'. Umgebungsvariablen und API-Keys werden in WSLs ~/.bashrc oder einer separaten env-Datei verwaltet.

3.5 Log-Erfassung

Das eigene Log des Task Schedulers ist wenig detailliert. Besser direkt im Skript umleiten:

# ~/agents/daily-pr-review.sh
#!/usr/bin/env bash
set -euo pipefail
LOG="$HOME/logs/pr-review-$(date +%Y%m%d).log"
mkdir -p "$(dirname "$LOG")"
exec >> "$LOG" 2>&1
echo "[$(date)] starte..."
# Agenten ausführen
claude-code --resume --task pr-review --quiet
echo "[$(date)] fertig."

4. KI-Agenten-spezifische Fallen

4.1 Leerer PATH — häufigste Ursache für „läuft nicht"

Der interaktive Shell-PATH unterscheidet sich vom launchd/cron-PATH. Den korrekten PATH am Anfang des Skripts explizit setzen:

#!/usr/bin/env bash
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"

Alternativ über EnvironmentVariables in der plist oder den PATH=-Header in der crontab.

Prüfen:

launchctl getenv PATH         # macOS
sudo -u appuser env | grep PATH  # Linux-cron-Benutzer

4.2 API-Keys niemals in plist/crontab eintragen

ANTHROPIC_API_KEY=sk-ant-... in einem plist-EnvironmentVariables-Block ist gefährlich:

  • Über ls -la einsehbar
  • Wird im Klartext in Time Machine / iCloud Backup gespeichert
  • Andere Benutzer auf demselben System können ihn per cat ~/Library/LaunchAgents/*.plist auslesen

Stattdessen:

  • macOS: Keychain + Skript liest per security find-generic-password -s anthropic-api -w
  • Linux: separate Datei ~/.config/agent/env mit chmod 600 + source im Skript
  • Windows: Get-Credential oder Credential Manager

Skript (macOS):

#!/usr/bin/env bash
set -euo pipefail
export ANTHROPIC_API_KEY="$(security find-generic-password -s anthropic-api -w 2>/dev/null)"
[ -z "$ANTHROPIC_API_KEY" ] && { echo "API-Key im Keychain fehlt"; exit 1; }
# ... Agenten ausführen

Key einmalig registrieren:

security add-generic-password -s anthropic-api -a "$USER" -w 'sk-ant-...'

4.3 Gleichzeitige Ausführungen verhindern — flock / Single-Instance-Sperre

Doppelte Ausführungen bedeuten doppelte API-Kosten und Race Conditions. Mit flock gegen gleichzeitige Läufe absichern:

#!/usr/bin/env bash
exec 200>/tmp/daily-pr-review.lock
flock -n 200 || { echo "läuft bereits"; exit 0; }
# ... Agenten ausführen

flock -n = nicht-blockierend; sofortiger Ausstieg, wenn die Sperre bereits gehalten wird.

4.4 Fehler nicht lautlos schlucken — Benachrichtigungen einrichten

Standardmäßig landet stderr in einer Logdatei — und bleibt dort unbemerkt. Mit trap aktiv benachrichtigen:

#!/usr/bin/env bash
set -euo pipefail
 
notify_failure() {
  local code=$?
  if [ $code -ne 0 ]; then
    # macOS — terminal-notifier oder osascript
    osascript -e "display notification \"PR-Review fehlgeschlagen (exit $code)\" with title \"Agent\""
    # Oder Slack-Webhook / E-Mail / Sentry
    # curl -X POST -H 'Content-Type: application/json' \
    #   -d "{\"text\":\"Agent fehlgeschlagen: exit $code\"}" "$SLACK_WEBHOOK_URL"
  fi
}
trap notify_failure EXIT
 
# Agenten ausführen

4.5 Nachholen nach Schlaf / Boot

  • macOS launchd: StartCalendarInterval holt einmal nach dem Aufwachen nach
  • Linux systemd timer: Persistent=true verwenden
  • Windows: Task Scheduler-Option „Aufgabe so bald wie möglich nach einem verpassten geplanten Start ausführen" aktivieren

Nur cron unterstützt kein Sleep-Nachholen — das ist der Hauptgrund, warum es auf Laptops kaum eingesetzt wird.


5. Echtes Szenario — Claude Code tägliches PR-Review

~/agents/daily-pr-review.sh:

#!/usr/bin/env bash
set -euo pipefail
 
# 1. PATH + Secrets
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
export ANTHROPIC_API_KEY="$(security find-generic-password -s anthropic-api -w)"
export GITHUB_TOKEN="$(security find-generic-password -s github-token -w)"
 
# 2. Sperre
exec 200>/tmp/daily-pr-review.lock
flock -n 200 || exit 0
 
# 3. Log
LOG="$HOME/logs/pr-review-$(date +%Y%m%d).log"
mkdir -p "$(dirname "$LOG")"
exec >> "$LOG" 2>&1
echo "[$(date)] starte tägliches PR-Review..."
 
# 4. Arbeit
cd "$HOME/code/my-repo"
git fetch --quiet origin
gh pr list --state open --json number,title,author --jq '.[] | select(.author.login != "me")' | \
  while read -r pr; do
    num=$(echo "$pr" | jq -r .number)
    echo "Überprüfe PR #$num..."
    claude-code --task "Review PR #$num — Fokus auf Sicherheit, Performance, fehlende Tests" --quiet
  done
 
echo "[$(date)] fertig."

Als LaunchAgent registrieren → läuft täglich automatisch um 09:00.


6. Fehlerbehebung

launchd „Service exited with abnormal code: 127"

Leerer PATH oder nicht ausführbares Skript. chmod +x ~/agents/*.sh ausführen und den PATH in der plist explizit setzen.

cron läuft nie und es gibt kein Log

  • crontab-Syntaxfehler: per crontab -l prüfen
  • Ohne MTA (mailutils) geht stderr verloren — explizit 2>&1 >> log umleiten
  • Ausführungsversuche mit journalctl -u cron (bzw. cronie) prüfen

Task Scheduler „Last Task Result: 0x1"

Das Skript wurde mit Exit-Code 1 beendet. Direkt in WSL debuggen und $LASTEXITCODE prüfen.

claude-code Befehl nicht gefunden

Dem Skript-PATH fehlt ~/.npm-global/bin oder das npm-Global-bin-Verzeichnis. Das Verzeichnis von which claude-code zum PATH hinzufügen.


Nächste Schritte

Referenzen

Changelog

  • 2026-05-12 — Erster Entwurf (devAlice M2 Seed-Erweiterung)