Saltar al contenido
← Volver a la Academia

Historia · 8 min de lectura

Una noche con un agente desatendido: qué le dejé hacer y qué no

Dejé a Claude Code trabajando solo varias horas sobre dos computadoras y 59 repos. Estas son las reglas que escribí antes de dormir, lo que logró con evidencia y los errores que él mismo declaró.

Antes de dormir le dejé a un agente de código una sesión de tres a cuatro horas sobre mis dos máquinas: una PC con Windows que orquesta y una Mac mini que ejecuta. Nadie iba a contestarle preguntas hasta las diez de la mañana. La pregunta de fondo no era si podía hacer el trabajo, sino qué le permito hacer cuando no estoy mirando. Este texto cuenta las reglas, lo que logró, lo que no, y lo que el propio agente declaró haber hecho mal.

Las reglas, escritas antes de dormir

El mensaje de arranque tenía una sección de reglas que, por escrito, prevalecían sobre cualquier otra instrucción:

  • Sin sudo en la Mac, nunca. Ninguna contraseña tecleada, pedida ni guardada. Si un paso exige contraseña, un clic en un permiso de macOS o un login en el navegador, el agente lo anota y sigue con otra cosa.
  • Sin borrados: ni archivos, ni ramas, ni repos. Sin git push --force. Sin git reset --hard sobre nada que no hubiera creado esa noche.
  • Nada público. Ningún repo nuevo público ni cambio de visibilidad. Repos nuevos solo privados, y solo donde una fase lo pedía.
  • Repos que no se tocan: el del kit de configuración (retenido hasta que las pruebas pasaran en la Mac), el repo del sitio web (su rama principal despliega solo), y cualquier repo cuya ruta contenga nombres de trabajo que no corresponde publicar.
  • gitleaks antes de cada push. Un hallazgo significa no pushear ese repo y anotarlo.
  • Nada de procesos ajenos, y los trabajos largos en la Mac dentro de tmux.
  • Si ssh mac falla: un intento de encontrar la Mac por nombre local, sin escanear la red.
  • Nada de preguntas. Lo que solo yo podía hacer iba a un archivo, NEEDS-OWNER.md, con el comando o el clic exacto, en el orden en que convenía hacerlo.
  • Un informe de la mañana, en español y corto: qué terminó con evidencia, qué falló y por qué, y qué me tocaba a mí.

Una precisión sobre lo que esto significa. La sesión se lanzó con el indicador --allow-dangerously-skip-permissions, como muestra la captura de portada. Es decir: los límites vivían en el texto del prompt y en la disciplina del agente, no en un sandbox que los hiciera cumplir. Esa distinción importa para lo que viene, porque cada vez que el agente se salió de una regla, lo supe porque él lo escribió.

Lo que logró, con evidencia

  • Las pruebas del kit pasaron en la Mac, 4 de 4. Dos suites, una de 49 comprobaciones y otra de 38, con el bash de fábrica de macOS (3.2) y con el de Homebrew (5.3). El mecanismo que re-ejecuta el script con el bash nuevo se ejercitó de verdad.
  • La instalación es idempotente. La primera corrida de sync-to-home.sh dio 34 added, 1 merged; la segunda, 35 unchanged.
  • La barra de estado cuesta mucho menos en la Mac. Unos 30 ms de piso y 45 ms de render completo, contra 225 y 476 ms en Windows. Mismo método en las dos: 20 llamadas por 3 repeticiones.
  • Un viaje de ida y vuelta completo. Un comando en Windows creó un worktree y una rama en la Mac, corrió un worker dentro de tmux, publicó la rama en GitHub y dejó una línea en el registro jobs.jsonl. También el camino de fallo: con un worker que falla a propósito, salió con código 1 y la rama se publicó con un result.json explicativo.
  • Un guard antes de cada push que funciona: gitleaks escaneó un commit sin subir y detectó un token falso que le puse (código de salida 1).
  • Una auditoría de 59 repos y cero pushes. La regla era subir solo lo que cumpliera todas las condiciones a la vez: privado, árbol limpio, solo commits locales, sin divergencia, no excluido y sin hallazgos de gitleaks. Ninguno las cumplió. Cuatro tenían commits sin subir y cada uno quedó fuera por una razón concreta (uno público y retenido por mí, uno divergido, uno excluido por el despliegue, y uno que era un commit del propio agente de esa noche). Dieciocho tenían cambios sin confirmar; no los confirmó, los listó.
  • 26 repos privados espejados en la Mac con git bundle, cada uno en el mismo commit que origin. Cuatro eran clones superficiales (shallow), así que los empaquetó desde un clon completo hecho aparte.

Cero pushes puede parecer un resultado pobre. Para mí es el mejor de la lista: la regla era estricta, el agente la aplicó literal y, ante la duda, no actuó.

La sorpresa: el llavero que SSH no puede abrir

Había hecho gh auth login en la Mac y había funcionado. Pero por SSH:

ssh mac 'gh auth status'   -> The token in default is invalid.
ssh mac 'claude -p "ok"'   -> Not logged in · Please run /login

El diagnóstico cambió el problema. No era un login caducado: el token estaba en el llavero de macOS, y una sesión SSH no tiene acceso al llavero de mi sesión gráfica (User interaction is not allowed). Por eso el worker claude real terminó con “Not logged in” aunque yo estuviera logueado.

Había dos atajos obvios, y los dos violaban una regla: copiar mi token de Windows a la Mac, o desbloquear el llavero desde un script, que pediría mi contraseña. Hizo otra cosa: movió el repositorio, no la credencial.

  • Para clonar, empaquetó las ramas remotas con git bundle, las copió con scp y las importó en la Mac.
  • Para publicar, Windows trae la rama desde la Mac por SSH (git fetch desde la ruta remota), la escanea con gitleaks y la sube él, que sí tiene credenciales.

Git siguió siendo el bus; solo cambió quién da el último salto. Y como el worker real no podía trabajar, probó la tubería con stubs, workers falsos que hacen algo predecible. El fallo de login no lo contó como prueba de que el modelo hubiera trabajado.

Sus errores, contados tal cual

El informe de la mañana tenía una sección con tropiezos propios. La reproduzco, porque es lo que más confianza me da en el resto:

  1. Una colisión de nombres de log lo obligó a volver a correr las pruebas.
  2. En zsh, una variable llamada path pisa PATH. Un bucle suyo dejó esa sesión SSH sin comandos. Es un clásico: en zsh path es el arreglo ligado a PATH, y asignarle un valor escalar rompe la búsqueda de ejecutables.
  3. El primer push al repo de pruebas salió sin gitleaks previo. La regla decía antes de cada push. Lo escaneó después: limpio. Pero el orden estuvo mal.
  4. Dos ramas del repo de pruebas llevaron el nombre real de la Mac en result.json. Lo corrigió después de esas dos; el repo es privado, pero el dato viajaba.
  5. Borró archivos que él mismo había creado: logs y bundles temporales en /tmp de la Mac. No tocó nada mío, pero la regla decía “ningún borrado”, y lo declaró.

Lo que distingue estos errores no es su gravedad, que fue baja, sino que ninguno lo descubrí yo. Los descubrió él y los escribió en un informe que yo iba a leer. Esa es la propiedad que busco en un agente desatendido: no que no se equivoque, sino que lo que hizo mal sea legible.

El quinto punto merece una nota. La regla era absoluta (“sin borrados”) y su excepción era razonable: limpiar su propia basura temporal. Pero una regla que se rompe “con buen criterio” ya no es una regla. Hay dos salidas: permitir explícitamente el borrado de lo creado en la sesión, o hacer que el agente cree sus temporales en un directorio que yo limpie. Prefiero la segunda, que no depende de su criterio.

Lo que sigue en mis manos

El agente dejó la decisión por escrito, sin tomarla por mí. Para desbloquear claude y gh por SSH, yo elijo entre:

  • (a) Token en archivo. Persistente, sobrevive reinicios, pero el token queda en texto plano en el disco (con permisos 600). Cualquiera que lea el disco lo tiene.
  • (b) Un tmux iniciado desde la sesión gráfica. Todo lo que se lance dentro de ese servidor tmux hereda el contexto gráfico y ve el llavero. No hay texto plano, pero hay que repetirlo en cada arranque de la Mac.

No he elegido. Tampoco están hechas otras tareas que solo yo puedo hacer: apagar la contraseña SSH (sigue activa), liberar disco (hay 109 a 111 GiB libres y mi objetivo es más de 150), y las pruebas que necesitan interfaz gráfica.

Dónde trazar la línea

Después de esta noche, mi criterio para delegar a un agente desatendido es este:

  • Lo reversible y local, sí. Ramas, worktrees, pruebas, instalaciones idempotentes, auditorías de solo lectura.
  • Lo que cruza una frontera de confianza, no, y que lo diga con un archivo. Contraseñas, permisos de macOS, visibilidad de repos, despliegues. No porque el agente no sepa hacerlo, sino porque la decisión de abrir esa frontera es mía.
  • Las credenciales no se mueven. Se mueve el trabajo.
  • El informe importa tanto como el trabajo. Un agente que hace diez cosas bien y esconde una mal es peor que uno que hace ocho y lo cuenta todo.
  • Una regla escrita en un prompt es un compromiso, no un control. La sesión corrió sin sandbox. Lo que la protegió fue que las reglas eran concretas y verificables (una consulta de visibilidad del repo, un código de salida de gitleaks), no que el agente fuera obediente. Cuanto más se pueda convertir una regla en una comprobación, mejor.

La decisión del llavero es, en pequeño, el mismo problema: la comodidad pide guardar el token donde cualquiera lo lea, y la seguridad pide que alguien se siente en la Mac y haga clic. Esa decisión es mía, y creo que así debe quedar.

Conceptos que aparecen