Ejecutar agentes de IA en segundo plano — cron · launchd · Programador de tareas
Patrones de programación para ejecutar agentes de IA como Claude Code periódicamente sin un humano frente al teclado. Comparativa de macOS launchd · cron en Linux · Programador de tareas de Windows.
Los agentes de IA como Claude Code no son solo para cuando estás frente al teclado. Revisión nocturna de PRs, clasificación de issues, sincronización de README, resumen de logs — el valor se acumula cuando se ejecutan de forma desatendida.
Creo que el mayor salto de productividad no viene de los agentes más inteligentes, sino de los que trabajan mientras duermes. Antes lo medíamos todo por velocidad de respuesta; ahora lo que importa es la cobertura: cuánto trabajo automatizado sucede porque así lo hemos configurado.
Esta guía compara las herramientas estándar de programación en tres sistemas operativos (macOS launchd, Linux cron, Programador de tareas de Windows) ante el mismo escenario, y destaca trampas específicas de los agentes de IA (seguridad de claves API, captura de logs, notificaciones de fallos).
TL;DR
| SO | Herramienta | Ubicación de la definición | Logs |
|---|---|---|---|
| macOS | launchd (LaunchAgent) | ~/Library/LaunchAgents/<etiqueta>.plist | StandardOutPath/StandardErrorPath |
| Linux | cron | crontab -e o /etc/cron.d/<nombre> | MTA o redirigir >> log.txt 2>&1 |
| Windows | Programador de tareas | GUI o PowerShell Register-ScheduledTask | Visor de eventos o redirigir a archivo |
Cinco trampas comunes:
- PATH vacío: comandos no encontrados
- Claves API almacenadas en texto plano en plist/crontab (NO hacer)
- Ejecuciones concurrentes del mismo trabajo compitiendo
- Muerte silenciosa: sin notificación de fallo
- Máquina dormida: tarea omitida por completo
Las secciones §1–§3 cubren patrones específicos por SO; §4, las trampas.
Requisitos previos
- Un agente de IA para ejecutar (por ejemplo, Claude Code, Cursor CLI, script personalizado)
- Claves API gestionadas vía env o un gestor de secretos
- Un script de shell (por ejemplo,
~/agents/daily-pr-review.sh)
1. macOS — launchd LaunchAgent — 10 min
cron existe en macOS, pero Apple recomienda launchd. Las ejecuciones de recuperación tras el sueño o el re-login también son más fiables.
1.1 Escribir el plist
~/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>
<!-- Entorno: PATH puede estar vacío, establecerlo explícitamente -->
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<!-- NO poner ANTHROPIC_API_KEY aquí — ver §4.2 -->
</dict>
<!-- Diariamente a las 09:00 -->
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key><integer>9</integer>
<key>Minute</key><integer>0</integer>
</dict>
<!-- Recuperación tras el sueño -->
<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 Registrar y ejecutar
# Registrar
launchctl load ~/Library/LaunchAgents/dev.local.daily-pr-review.plist
# Ejecutar inmediatamente (saltarse la programación)
launchctl start dev.local.daily-pr-review
# Estado
launchctl list | grep daily-pr-review
# Cancelar el registro
launchctl unload ~/Library/LaunchAgents/dev.local.daily-pr-review.plist1.3 Recuperación tras la suspensión
cron omite las ejecuciones que ocurren durante la suspensión. launchd ejecutará una vez al reactivarse (nota: StartCalendarInterval solo recupera una ejecución).
Para recuperación horaria, etc.:
<key>StartInterval</key>
<integer>3600</integer> <!-- cada hora -->→ Se ejecuta una vez al reactivarse, a diferencia de cron.
2. Linux — cron — 5 min
2.1 Editar crontab
crontab -eEjemplo:
# m h dom mon dow comando
0 9 * * * /home/user/agents/daily-pr-review.sh >> /home/user/logs/pr-review.log 2>&1| Campo | Valor | Significado |
|---|---|---|
0 | 0 | en punto |
9 | 9 | 9 am |
* * * | todos | cada día |
Diariamente a las 09:00, añade stdout/stderr a un archivo de log.
2.2 Establecer PATH explícitamente
El PATH de cron es muy reducido. Establecerlo en el script o al inicio del crontab:
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>&12.3 cron del sistema (/etc/cron.d/)
Para registro a nivel root en servidores:
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
EOFEl campo appuser especifica el usuario de ejecución. Importante para la seguridad.
2.4 Temporizador systemd (alternativa moderna)
Una alternativa a cron. Los archivos de unidad unifican el reinicio, el manejo de fallos y el registro.
# /etc/systemd/system/pr-review.service
[Unit]
Description=Agente de revisión diaria de PRs
[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=Ejecutar revisión diaria de PRs a las 09:00
[Timer]
OnCalendar=*-*-* 09:00:00
Persistent=true # ejecutar tras el arranque si el tiempo pasó durante la inactividad
[Install]
WantedBy=timers.targetsudo systemctl enable --now pr-review.timer
journalctl -u pr-review.service # logsPersistent=true es la gran ventaja sobre cron — recuperación automática tras inactividad del servidor.
3. Windows — Programador de tareas — 10 min
La GUI funciona, pero es mejor hacerlo reproducible vía PowerShell.
3.1 Registrar mediante PowerShell
# Acción
$action = New-ScheduledTaskAction `
-Execute "wsl.exe" `
-Argument "-d Ubuntu -e bash -lc '~/agents/daily-pr-review.sh'"
# Disparador: diariamente a las 09:00
$trigger = New-ScheduledTaskTrigger -Daily -At 9am
# Configuración: ejecutar aunque el usuario no esté conectado; despertar del sueño
$settings = New-ScheduledTaskSettingsSet `
-StartWhenAvailable `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-ExecutionTimeLimit (New-TimeSpan -Hours 1) `
-RestartCount 2 `
-RestartInterval (New-TimeSpan -Minutes 10)
# Registrar
Register-ScheduledTask `
-TaskName "DailyPRReview" `
-Action $action `
-Trigger $trigger `
-Settings $settings `
-Description "Run Claude Code daily PR review in WSL"3.2 Confirmar en la interfaz gráfica
Win + R → taskschd.msc → Biblioteca del Programador de tareas → "DailyPRReview".
3.3 Probar de inmediato
Start-ScheduledTask -TaskName "DailyPRReview"
Get-ScheduledTaskInfo -TaskName "DailyPRReview" | Select LastRunTime, LastTaskResultLastTaskResult: 0 significa éxito.
3.4 Ejecutar en WSL
Como se muestra arriba: wsl.exe -d Ubuntu -e bash -lc '...'. Las variables de entorno y las claves API deben definirse en el ~/.bashrc de WSL o en un archivo de env separado.
3.5 Captura de registros (logs)
El registro propio del Programador de tareas es escaso. Redirigir dentro del script:
# ~/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)] iniciando..."
# Ejecutar el agente
claude-code --resume --task pr-review --quiet
echo "[$(date)] completado."4. Trampas específicas de los agentes de IA
4.1 PATH vacío — causa n.º 1 de "no se ejecuta"
PATH de shell interactivo ≠ PATH de launchd/cron. Declararlo al inicio del script:
#!/usr/bin/env bash
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"O vía las variables de entorno EnvironmentVariables / PATH= en plist/crontab.
Verificar:
launchctl getenv PATH # macOS
sudo -u appuser env | grep PATH # usuario de cron en Linux4.2 No poner claves API en plist/crontab
ANTHROPIC_API_KEY=sk-ant-... dentro de un bloque EnvironmentVariables de plist:
- Expuesto con
ls -la - Almacenado en Time Machine / iCloud Backup en texto plano
- Otros usuarios de la misma máquina pueden leerlo vía
cat ~/Library/LaunchAgents/*.plist
En su lugar:
- macOS: Keychain + el script lee vía
security find-generic-password -s anthropic-api -w - Linux: archivo separado
~/.config/agent/envconchmod 600+sourceen el script - Windows:
Get-Credentialo el Administrador de credenciales
Script (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 "falta la clave API en Keychain"; exit 1; }
# ... ejecutar agenteRegistrar la clave una sola vez:
security add-generic-password -s anthropic-api -a "$USER" -w 'sk-ant-...'4.3 Prevenir ejecuciones concurrentes con flock / instancia única
Ejecuciones dobles = 2× coste de API + condición de carrera. Bloquear con flock:
#!/usr/bin/env bash
exec 200>/tmp/daily-pr-review.lock
flock -n 200 || { echo "ya en ejecución"; exit 0; }
# ... ejecutar agenteflock -n = no bloqueante; sale inmediatamente si está bloqueado.
4.4 No fallar en silencio: notificaciones de fallo
Por defecto, stderr termina en un archivo de log y punto. Usar trap para notificar:
#!/usr/bin/env bash
set -euo pipefail
notify_failure() {
local code=$?
if [ $code -ne 0 ]; then
# macOS — terminal-notifier u osascript
osascript -e "display notification \"Revisión de PR fallida (salida $code)\" with title \"Agente\""
# O webhook de Slack / email / Sentry
# curl -X POST -H 'Content-Type: application/json' \
# -d "{\"text\":\"Agente fallido: salida $code\"}" "$SLACK_WEBHOOK_URL"
fi
}
trap notify_failure EXIT
# Ejecutar agente4.5 Recuperación tras la suspensión o el arranque
- launchd de macOS:
StartCalendarIntervalrecupera una ejecución tras la reactivación - Temporizador systemd de Linux: usar
Persistent=true - Windows: Programador de tareas → "Ejecutar la tarea tan pronto como sea posible si se pierde un inicio programado" ACTIVADO
Solo cron carece de recuperación ante la suspensión: por eso nadie usa cron en portátiles.
5. Escenario real: revisión diaria de PRs con Claude Code
~/agents/daily-pr-review.sh:
#!/usr/bin/env bash
set -euo pipefail
# 1. PATH + secretos
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. Bloqueo
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)] iniciando revisión diaria de PRs..."
# 4. Trabajo
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 "Revisando PR #$num..."
claude-code --task "Revisar PR #$num — centrarse en seguridad, rendimiento, pruebas ausentes" --quiet
done
echo "[$(date)] completado."Registrar como LaunchAgent → se ejecuta automáticamente todos los días a las 09:00.
6. Solución de problemas
launchd: "Service exited with abnormal code: 127"
PATH vacío o script no ejecutable. chmod +x ~/agents/*.sh + establecer PATH en el plist.
cron nunca se ejecuta y no hay registros
- Error de sintaxis en crontab: verificar con
crontab -l - Sin MTA (mailutils), stderr se pierde → usar explícitamente
2>&1 >> log - Inspeccionar intentos de ejecución con
journalctl -u cron(o cronie)
Programador de tareas: "Last Task Result: 0x1"
El script salió con código 1. Depurar ejecutándolo directamente dentro de WSL. Inspeccionar $LASTEXITCODE.
claude-code no encontrado
~/.npm-global/bin o el directorio bin global de npm no está en el PATH del script. Añadir el directorio de which claude-code al PATH.
Pasos siguientes
- Configuración de Claude Code — requisito previo
- Hooks de Claude Code — patrones de automatización de eventos
- Flujo de trabajo multi-agente — colaboración entre múltiples agentes
Referencias
Registro de cambios
- 2026-05-12 — Borrador inicial (devAlice M2 seed expansion)