9 de junio de 2026
macOSlaunchdAutomationDevOps

Deja de Usar cron en tu Mac: Usa launchd en su Lugar

cron consulta según un horario y se pierde trabajos mientras tu portátil duerme. WatchPaths de launchd reacciona a eventos del sistema de archivos en su lugar, y es la automatización que reemplazó a cron para siempre.

8 min de lectura

Necesitaba que mi máquina reaccionara a los cambios, no que los consultara. La respuesta fue una función de macOS de 20 años que casi nadie usa, y superó a cron en todo lo que importaba.

La semana pasada construí un sistema que hace auto-commit de la configuración de mi máquina: instalo un paquete de Homebrew, y ocho segundos después hay un commit de git en GitHub que lo registra.

Cuando empecé, mi memoria muscular decía lo que dice la memoria muscular de todo desarrollador:

Ponlo en cron.

No lo hice. Y las razones se convirtieron en la lección más útil sobre macOS que he aprendido en años.

⏰ El Problema de Consultar Constantemente

Así es como se ve la versión con cron de mi automatización:

*/5 * * * * /Users/me/sync.sh

cron consultando cada cinco minutos frente a launchd disparándose con eventos del sistema de archivos

Cada cinco minutos, para siempre, mi máquina despierta un proceso para preguntar: ¿ha cambiado algo?

Casi siempre, la respuesta es no.

Esa es la forma fundamental de cron: consultar. No sabe nada de eventos. Solo sabe de tiempo. Así que te obliga a una mala disyuntiva:

  • Consultar con frecuencia → cientos de ejecuciones inútiles al día, por si acaso
  • Consultar rara vez → los cambios quedan sin commitear durante una hora, anulando el propósito

Y cron en un portátil tiene un segundo problema, más traicionero. cron asume que la máquina está encendida. Cierra la tapa a las 11:58, y el trabajo de las doce simplemente no ocurre. Sin reintento, sin ponerse al día, nada. Los servidores siempre están encendidos; los portátiles duermen. cron se diseñó para servidores.

Lo que en realidad quería era sencillo de expresar:

Cuando el sistema de archivos cambie, ejecuta mi script. Si no, no hagas absolutamente nada.

Eso no es un horario. Es un evento.

⚙️ El Daemon que Ya Tienes

macOS trae launchd de fábrica, es literalmente el proceso 1, lo que arranca cada servicio del sistema. Apple reemplazó cron internamente por él hace años (los trabajos de cron de tu Mac son en realidad ejecutados por launchd).

La mayoría de los desarrolladores sabe que launchd existe. Casi nadie usa su mejor función:

WatchPaths, launchd vigilará una ruta del sistema de archivos a nivel del sistema operativo, y disparará tu trabajo en el momento en que cambie.

<key>WatchPaths</key>
<array>
    <string>/opt/homebrew/Cellar</string>
    <string>/opt/homebrew/Caskroom</string>
</array>

Ese es todo el truco. Homebrew instala cada paquete en /opt/homebrew/Cellar, así que cualquier instalación o desinstalación, desde cualquier shell, script o herramienta, cambia ese directorio. El sistema lo nota. Mi script se ejecuta.

Sin consultas. Sin ejecuciones desperdiciadas. Cero CPU en reposo. El trabajo existe como una intención registrada, y el kernel es quien vigila.

La definición completa del trabajo es un solo archivo plist:

<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.paurushrai.dotfiles-sync</string>
 
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>/Users/paurushrai/Developer/infra/dotfiles/sync.sh</string>
    </array>
 
    <key>WatchPaths</key>
    <array>
        <string>/opt/homebrew/Cellar</string>
    </array>
 
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key><integer>12</integer>
        <key>Minute</key><integer>0</integer>
    </dict>
 
    <key>ThrottleInterval</key>
    <integer>60</integer>
 
    <key>StandardOutPath</key>
    <string>/Users/paurushrai/Library/Logs/dotfiles-sync.log</string>
</dict>
</plist>

Déjalo caer en ~/Library/LaunchAgents/, regístralo una vez:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.paurushrai.dotfiles-sync.plist

Sobrevive a los reinicios. Se ejecuta esté o no abierta una terminal. Ahora es parte del sistema operativo.

Lo probé de la forma honesta, instalé un paquete y no toqué nada más:

$ brew install jq
... 8 seconds later, zero shell involvement:
67ba27e chore: auto-sync .Brewfile  → pushed to GitHub ✅

brew install jq disparando un auto-commit subido a GitHub en ocho segundos

🧩 Tres Claves que Hacen Todo el Trabajo Pesado

El plist de arriba tiene tres decisiones de diseño incorporadas, y cada una reemplazó código que de otro modo habría tenido que escribir.

las tres claves de launchd: ThrottleInterval, StartCalendarInterval y StandardOutPath

ThrottleInterval: 60 es un antirrebote gratis. Instala cinco paquetes seguidos y el sistema de archivos dispara cinco eventos, pero launchd no ejecutará el trabajo más de una vez cada 60 segundos, así que obtengo un solo commit que cubre los cinco. En el mundo de cron, agrupar así es lógica que tienes que escribir y probar tú mismo. Aquí es un entero.

StartCalendarInterval es el horario de cron, menos el problema de cron con los portátiles. Me da una ejecución diaria al mediodía como red de seguridad para cualquier cosa que las vigilancias se pierdan. Y a diferencia de cron, si la máquina está dormida al mediodía, launchd ejecuta el trabajo cuando despierta. Los horarios perdidos se ejecutan tarde en vez de nunca. Para un portátil, ese único comportamiento zanja la comparación.

StandardOutPath es observabilidad gratis. La salida de cada ejecución se añade a un archivo de log que yo elegí. Cuando me pregunté por qué algo no se había disparado, la respuesta ya estaba escrita.

🔁 La Trampa que Pilla a Todo el Mundo

Ahora la confesión. Mi script hace commit a un repo de git, y mi primer instinto fue vigilar el propio directorio del repo. Una sola entrada de vigilancia, cubre todos los archivos. Elegante.

También es un bucle infinito.

el bucle de retroalimentación: un commit muta .git, lo que dispara el agente, que vuelve a hacer commit

Cada commit de git muta .git/. Así que: un cambio dispara el agente → el agente hace commit → el commit cambia .git/ → que está dentro del directorio vigilado → que dispara el agente otra vez. Para siempre.

El arreglo es una regla que vale la pena memorizar:

Nunca vigiles un directorio en el que tu trabajo escribe.

En su lugar vigilo archivos individuales, cada dotfile rastreado tiene su propia entrada en WatchPaths, y la raíz del repo está deliberadamente ausente. El trade-off: un archivo nuevo en un subdirectorio profundo no se detecta al instante; la ejecución diaria del mediodía lo recoge. Un precio justo por un sistema que no puede perseguirse la cola.

Dos trampas más de la misma tarde, para que no pagues tú mi matrícula:

  • No hagas symlink del plist. Guardo el plist en mi repo de dotfiles y lo copio a ~/Library/LaunchAgents/, launchd no es fiable con definiciones de trabajo enlazadas simbólicamente en el macOS moderno. El repo guarda la fuente de verdad; el instalador despliega una copia.
  • Los trabajos de launchd reciben un entorno mínimo. Sin el PATH de tu shell, sin alias, nada de tu .zshrc. Mi script establece su propio PATH en la primera línea. Si tu trabajo "funciona en la terminal pero no en launchd", casi siempre es esto.

🔔 Hazlo Hablar, Luego Hazlo Esperar

Un trabajo en segundo plano que no puedes ver es un trabajo en segundo plano en el que dejarás de confiar.

Mi script anuncia cada commit con una línea nativa de macOS:

osascript -e 'display notification "Committed & pushed: .Brewfile" with title "dotfiles sync"'

Primera prueba: nunca la vi. Los banners de notificación se desvanecen en cinco segundos.

El arreglo no fue código, fue un ajuste que casi nadie cambia: Ajustes del Sistema → Notificaciones → Script Editor → Alertas. Las notificaciones tipo alerta permanecen en pantalla hasta que las descartas. Ahora cada sincronización espera en la esquina a que la reconozca, con los nombres de los archivos cambiados en el mensaje.

Detalle de diseño que importa: el script solo notifica cuando algo se commiteó de verdad. Una ejecución sin cambios es silenciosa. Una notificación que se dispara constantemente es una notificación que ya has aprendido a ignorar, la fatiga de alertas mata la observabilidad más rápido que no tener alertas en absoluto.

🛠 La Chuleta

Todo lo que necesitas para tener la tuya:

# register (once, or after editing the plist)
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.myjob.plist
# unregister
launchctl bootout gui/$(id -u)/com.example.myjob
# is it loaded?
launchctl list | grep myjob
# what happened?
tail -f ~/Library/Logs/myjob.log

Y la tabla de decisión que me hubiera gustado tener:

  • El trabajo debe reaccionar a que cambie un archivo o directorio → WatchPaths
  • El trabajo debe ejecutarse según un horario, seguro para portátiles → StartCalendarInterval
  • El trabajo se dispara en ráfagas que deberían agruparse → ThrottleInterval
  • El trabajo necesita ejecutarse en el momento en que se registra → RunAtLoad
  • Estás a punto de escribir crontab -e en un Mac → para, escribe un plist

🎯 La Lección Detrás de la Lección

La automatización que construí es pequeña. El principio no lo es.

Estuve a punto de recurrir a cron porque cron es lo que conocía, y estuve a punto de recurrir a un paquete npm de vigilancia de archivos porque es lo que sugieren los resultados de búsqueda. Ambos habrían funcionado, mal: uno consultando según un temporizador, el otro añadiendo una dependencia para hacer lo que el sistema operativo ya hace de forma nativa.

La herramienta aburrida, incorporada, de veinte años, fue mejor que las dos. Guiada por eventos en vez de por consultas. Consciente de los portátiles en vez de asumir servidores. Cero dependencias en vez de una cosa más que instalar en la próxima máquina.

Antes de añadir una herramienta, pregunta qué es lo que el sistema operativo ya trae de serie.

La respuesta es, más a menudo de lo que crees: todo lo que necesitabas, esperando en el proceso 1 desde 2005.

¿Qué se está ejecutando en tu carpeta LaunchAgents?

Tengo curiosidad por saber qué han automatizado otros a nivel del sistema operativo, y con qué trampas se han topado al hacerlo. El bucle de retroalimentación de WatchPaths no puede ser la única.