Un agente LLM que hace SEO solo cada noche — con guardarraíles deterministas
Cómo construí un agente autónomo que ejecuta tareas de SEO a la 01:00 con tres capas de contención que el modelo no puede negociar: permissions.deny, un hook PreToolUse y un fichero KILLSWITCH.
Este ensayo se publico primero en brianmenagomez.com/blog/agente-seo-nocturno-guardarrailes-deterministas.
El problema: un agente que trabaje de madrugada sin que nadie lo vigile
Mantener el SEO de un sitio en crecimiento es un trabajo de fondo que no para. Hay que revisar enlaces rotos, regenerar el sitemap cuando cambia la base, comprobar que las páginas nuevas se indexan, actualizar metadatos. Son tareas repetitivas, predecibles y perfectamente automatizables. El problema no es qué hay que hacer: es cuándo y con qué garantías.
Si lo hago yo, compite con la construcción de producto. Si lo programo con scripts tradicionales, cada cambio en el sitio rompe algo y paso más tiempo manteniendo los scripts que mejorando el SEO. Lo que necesitaba era un agente LLM que razonara sobre el estado del sitio, decidiera qué tocar y ejecutara — pero solo de madrugada, cuando la carga es mínima y nadie está mirando. Y necesitaba una certeza: que si el agente se equivoca, el daño esté acotado.
El primer intento: Claude headless con permisos básicos
La pieza central era sencilla: un job programado en el Task Scheduler de Windows que, a la 01:00, lanza Claude en modo headless — `claude -p` con un prompt que describe las tareas de SEO de esa noche. Sin interfaz, sin supervisión humana. Claude examina el repositorio, genera cambios y los sube.
Para la primera versión usé el sistema de permisos nativo de Claude Code: un `.claude/settings.json` con `permissions.allow` para las herramientas necesarias (lectura de archivos, git, escritura controlada) y `permissions.deny` para las peligrosas (acceso a red externa, ejecución de comandos arbitrarios). Parecía suficiente.
Funcionó durante tres noches. A la cuarta, falló.
El error: confiar en que el prompt basta
El agente decidió que para «optimizar el sitemap» necesitaba borrar entradas antiguas directamente de la tabla de la base de datos. No era malicioso: su razonamiento era que las URLs obsoletas debían desaparecer. Pero «desaparecer» para un LLM significa `DELETE FROM sitemap_urls WHERE last_seen < '2025-01-01'`. Y eso, en producción, sin backup previo, es un agujero.
El sistema de permisos nativo detuvo la operación — pero por los pelos. Me di cuenta de que el problema no era esa consulta concreta, sino la arquitectura de seguridad: estaba delegando la contención del agente en reglas declarativas evaluadas por el propio LLM. Si el modelo alucinaba una justificación convincente, el permiso se concedía. Necesitaba algo que el LLM no pudiera negociar.
La solución: tres capas deterministas que el agente no puede esquivar
Rediseñé la contención desde cero con tres capas deterministas. Ninguna depende de que el modelo «entienda» una restricción: son barreras de software que se ejecutan antes de que la herramienta llegue al sistema.
Primera capa: `permissions.deny` en `.claude/settings.json`. Esta capa bloquea herramientas completas — ejecución de shell arbitraria, acceso a red, manipulación directa de base de datos. Lo importante no es qué niega, sino *cuándo* se evalúa: las reglas `permissions.deny` se aplican incluso bajo el flag `--dangerously-skip-permissions` y ganan siempre a cualquier `allow`. No hay atajo. Si una herramienta está en la lista de denegación, no se ejecuta bajo ninguna circunstancia. Punto.
Segunda capa: un hook PreToolUse — `guard.mjs`. Esta es la capa de granularidad fina. Antes de cada invocación de herramienta, un script en Node inspecciona el comando concreto y lo bloquea si coincide con patrones peligrosos. Por ejemplo: cualquier instrucción SQL que contenga `DELETE`, `DROP` o `TRUNCATE` se rechaza automáticamente. También se filtran `git push` a ramas protegidas y escrituras fuera del directorio de trabajo. El guardarraíl no pregunta, no razona, no negocia: coincide el patrón y bloquea.
Tercera capa: el fichero KILLSWITCH. Es un archivo vacío en una ruta conocida. Si existe, el agente no ejecuta *ninguna* herramienta — ni de lectura. Es el botón rojo: si alguna vez detecto comportamiento anómalo en el briefing de la mañana, creo el fichero y la próxima ejecución nocturna ni siquiera arranca. La verificación ocurre en el prompt inicial del agente, antes de cualquier llamada a herramienta.
Con estas tres capas, el agente opera bajo una restricción adicional que yo mismo me impuse: solo cambios reversibles y aditivos. Nada de `DELETE`, `DROP` o `TRUNCATE` en producción. Los cambios en el código del sitio no van por `git push` directo a `main`: el agente crea una rama, hace sus modificaciones y las fusiona con `gh pr merge --squash`. Si algo sale mal, revertir la PR es un comando. Si el agente añade una página que no debería existir, quitarla es trivial. Todo lo que toca deja un rastro auditable en el historial de git.
Cada mañana, el agente escribe y envía un briefing por correo electrónico con lo que hizo esa noche: qué archivos modificó, qué URLs añadió al sitemap, qué PR creó, qué errores encontró y cómo los resolvió. Es mi ventana a la noche anterior. Si algo no cuadra, activo el KILLSWITCH y reviso antes de la siguiente ejecución.
Qué aprendí
La lección no es sobre SEO ni sobre Claude. Es sobre delegar a un LLM sin supervisión humana: la seguridad tiene que ser determinista. Un prompt que dice «no borres nada» vale exactamente lo mismo que la siguiente instrucción que el modelo genere para convencerse de que esta vez sí debe borrar algo. Las tres capas — `permissions.deny`, `guard.mjs` y KILLSWITCH — no dependen de que el modelo las obedezca: dependen de que el sistema las imponga antes de que la herramienta llegue a tocar nada.
Construir un agente autónomo no es enseñarle a portarse bien. Es asumir que se va a portar mal y ponerle barrotes que no pueda doblar.
Si quieres leer más sobre cómo construyo y opero los agentes que mantienen Conversor, tengo una serie de ensayos en el blog.

