Volver a artículos
CategoríaDemos

Cómo corro 10 agentes de Claude Code en paralelo sin tmux ni headless

24 de julio de 2026
Tags
Inteligencia ArtificialLLMAutomatizaciónProductividadOpen Source
Cómo corro 10 agentes de Claude Code en paralelo sin tmux ni headless

Hero

En el GIF de apertura: diez pestañas de kitty, ese terminal del ícono de gato, cada una con un Claude Code en plan mode corriendo dentro de su propia git worktree. Un solo comando disparó todo. Me quedé mirando los planes nacer en paralelo, uno por pestaña, con Fable coordinando la ronda mientras yo resolvía otra cosa.

Tiempo estimado de lectura: 6 min. Sales de aquí sabiendo qué problema me llevó a construir la skill orchestrating-terminal-agents y qué se rompió en el camino. Si usas Claude Code todos los días y ya quisiste soltar varios agentes a la vez sin perder el control, quédate.

El Problema

Tengo una startup personal, Ondokai, que vengo construyendo desde hace casi un año. Ahí dentro hay mucha tarea que se paraleliza con Claude Code, y yo lo hacía a mano: crear la worktree, abrir el agente, esperar a que terminara, commitear cuando él no commiteaba, mergear de vuelta a la branch y ajustar conflictos, a veces a mano, a veces con IA. Cada ronda de esas me sacaba del flow. Doloroso.

Correr un agente de código es fácil. Correr varios en paralelo, con aislamiento real y sin perder el control, no lo es. Dos agentes en el mismo working tree se atropellan, un orquestador headless te esconde lo que el agente está haciendo, y apilar tmux en medio solo agrega capa.

(Paréntesis para quien nunca vio git worktree, disclaimer rápido: es un segundo working tree del mismo repositorio, con branch propia, sin clonar nada de nuevo. Dos agentes editan el mismo proyecto sin pisarse entre sí. Dicho eso, seguimos.)

La Solución

Básicamente creé una manera, una skill, de que Claude Code levante hordas de agentes en pestañas de kitty y siga el trabajo hasta que el merge vuelva. Es una Agent Skill y un par de CLIs independientes, así que cualquier script (o cualquier agente de IA en un terminal) puede usarla. El repo: orchestrating-terminal-agents.

Y aquí la parte que casi nadie cree: no necesitas entender el meollo. La skill está escrita para que la lea la IA, no tú. La instalas en tu máquina, la referencias en el prompt ("usa la skill orchestrating-terminal-agents") y listo, sea en Claude Code, Copilot, Gemini CLI, Kimi, Open Code o pi coding agent.

bash
# terminal: 3 agentes en plan mode, cada uno en una worktree nueva, prompt ya enviado
fan 3 "refactoriza el módulo de auth" --repo ~/Projects/mi-app

# eligiendo todo: instalación, modelo, mode, effort
fan 2 "..." --repo ~/Projects/mi-app --profile dsa --model 'opus[1m]' --permission-mode plan --effort max

# un prompt DIFERENTE por agente (el nº de prompts define el nº de agentes)
python3 scripts/fanout.py --repo ~/Projects/mi-app \
  --prompt "encárgate del auth" --prompt "encárgate del caché" --prompt "encárgate de los logs"

El fan es un alias bien flaco encima de scripts/fanout.py. La primera forma cubre casi todo mi uso; las otras existen porque algún día las necesité.

Funcionalidades

  1. Fan-out de verdad: N pestañas de kitty, cada una con su worktree (<padre>/<repo>.worktrees/<branch>) y su branch. Cada worktree graba branch.<b>.origdir y .origbranch en el git config, así que el flujo de finalización sabe de dónde salió y a dónde vuelve en el merge.
  2. Un prompt por agente, si quieres: un solo --prompt se vuelve misión compartida; N prompts se vuelven N misiones distintas en la misma llamada. Así mandé a uno a cuidar el auth, a otro el caché, a otro los logs.
  3. Todo por flag: instalación de Claude Code (--profile, encima de CLAUDE_CONFIG_DIR), modelo (opus, fable, sonnet, haiku, opus[1m]), permission mode (default plan) y effort (default max).
  4. Visible, no headless: cada agente corre en una pestaña que tú ves. Puedes leer el plan mientras se escribe, teclear en medio, interrumpir con ESC y tomar la sesión cuando quieras.
  5. Primitivas que controlan cualquier TUI: el kittyctl.py abre pestañas, teclea texto literal, manda tecla simbólica, espera a que la pantalla haga match con un regex y lee la pantalla de vuelta. Agente es solo uno de los casos de uso.
  6. Yolo prendido por default: todo claude sube con --dangerously-skip-permissions, a menos que pases --no-yolo. Defiendo la elección ahí abajo.

Stack Técnico

  • "Frontend": las pestañas de kitty. La UI es el terminal mismo, con user vars en cada ventana para un matcher estable (var:fan=<run_id>).
  • Backend: dos scripts de Python solo con stdlib, kittyctl.py (primitivas) y fanout.py (la receta), hablando con el remote control nativo de kitty (kitty @) a través de socket Unix. Nada de tmux o Zellij: una capa menos de RAM y redraw.
  • Infra: git worktree para aislamiento, zsh para los aliases y el wrapper de retry, Claude Code (probado en la v2.1.218) resuelto por FANOUT_CLAUDE_BIN, PATH o ~/.local/bin/claude. Desarrollado en Pop!_OS 24.04 (X11), kitty 0.48, cero dependencia de pip.

El único requisito de config es habilitar el remote control:

ini
# ~/.config/kitty/kitty.conf
allow_remote_control socket-only   # NUNCA 'yes': con yes, una escape sequence impresa en el terminal controla kitty
listen_on            unix:@mykitty

El socket-only importa el doble cuando corres agentes de IA: con yes, un cat sobre un archivo malicioso podría abrir pestañas y ejecutar comandos. Y reinicia kitty después de editar, porque una instancia ya abierta no crea el socket.

El Desafío Más Interesante

El bug más desleal fue el exit code que miente. kitty @ send-key y send-text salen con exit 0 aunque no hagan match con ninguna ventana. Es arquitectural: disallow_responses en el código fuente de kitty. O sea, tu script teclea al vacío, recibe éxito y sigue como si nada. Falla en silencio, la peor especie.

La salida fue cambiar la confianza del envío por la confianza de la pantalla. El kittyctl verifica antes cada matcher con ls y aborta en vez de mentir éxito; después del envío, quien confirma es el wait-for, que reemplaza el sleep ciego por un regex con timeout:

bash
# terminal: manejando un REPL, cada paso verificado por la pantalla
kctl tab-new --var app=py --cwd /tmp -- python3
kctl wait-for  --match var:app=py --regex '>>> '
kctl send-text --match var:app=py --text 'print(6*7)'
kctl send-key  --match var:app=py enter
kctl capture   --match var:app=py | tail -2

Fíjate en la separación entre send-text y send-key enter. Es la regla 2 de la skill: Claude Code prende el bracketed paste (DECSET 2004), y un \r al final del mismo send-text cae dentro del sobre de pegado en vez de enviar el prompt. La tecla especial sigue la misma lógica: siempre nombre simbólico (shift+tab, escape), nunca escape crudo, porque kitty codifica según el modo que la TUI negoció.

Hay más gotchas tratados en el repo: el Shift+Tab que cicla los modos de Claude Code (3 teclas, y el orden cambia entre versiones), el diálogo de confianza que bloquea toda worktree nueva, el ambiente CLAUDE_* que se filtra del agente padre al hijo. El LEARNINGS.md es la memoria episódica de todo eso.

Lo Que Aprendí

La orquestación buena es la que devuelve atención. Mi caso real: escribí las tareas en el prompt principal, le pedí a Claude usar la skill para disparar las ventanas y le dije que solo esperara hasta que todas las worktrees volvieran mergeadas. Cada prompt hijo ya cargaba resolución de la tarea, commit, push y merge back. Corrieron 10 agentes simultáneos, independientes, y los 10 volvieron. Yo coordinaba con Fable y tenía respuesta automática configurada en hasta 5 minutos: si yo no respondía, él tomaba la recommendation y seguía. La mejor paralelización que he tenido, por lejos.

Sobre el yolo por default, mi defensa honesta: corre todo dentro de un docker o VM y el riesgo sale de escena. Dar permiso progresivo es una ilusión, porque llega un momento en que dejas de leer los prompts. Te vuelves el cuello de botella. Para mí vale mucho más revisar el diff de una branch grande al final que estar haciendo clic en opciones de permiso todo el día en medio de otras demandas. Un prompt bien estructurado y los guardrails correctos en el claude.md aguantan al agente mejor que mi clic cansado.

Y el trade-off de flags contra teclas: automatizar una TUI por inyección de tecla es frágil, lleno de races y teclas que se pierden en silencio. Siempre que existe flag de CLI, la skill prefiere flag (--permission-mode plan en vez de ciclar Shift+Tab). Tecla solo cuando no queda otra, y ahí con verificación de pantalla en cada paso. Una lata escribirlo una vez. Confiable para siempre.

¡Pruébalo!

bash
# terminal: clona, linkea como skill global de Claude Code y aliases
git clone https://github.com/frederico-kluser/orchestrating-terminal-agents.git ~/Projects/orchestrating-terminal-agents

mkdir -p ~/.claude/skills
ln -s ~/Projects/orchestrating-terminal-agents ~/.claude/skills/orchestrating-terminal-agents

fan()  {
  if [ $# -lt 2 ]; then echo 'uso: fan <N> <prompt> [opciones]' >&2; return 2; fi
  python3 ~/Projects/orchestrating-terminal-agents/scripts/fanout.py --count "$1" --prompt "$2" "${@:3}"
}
kctl() { python3 ~/Projects/orchestrating-terminal-agents/scripts/kittyctl.py "$@"; }

Instálalo, abre Claude Code y pide: "usa la skill orchestrating-terminal-agents para disparar 3 agentes en este repo". Empieza con 2 o 3, no con 10. Y después me cuentas.

Si algo truena, abre un issue con lo que apareció en pantalla, que el capture existe para eso. Y si la skill te ahorra una tarde de worktree manual, manda este post al Slack o al WhatsApp del equipo: alguien ahí está mergeando conflictos a mano en este mismo momento.