Como eu rodo 10 agentes Claude Code em paralelo sem tmux nem headless

Hero
No GIF de abertura: dez abas do kitty, aquele terminal do ícone de gato, cada uma com um Claude Code em plan mode rodando dentro da sua própria git worktree. Um comando disparou tudo. Eu fiquei olhando os planos nascerem em paralelo, um por aba, com o Fable coordenando a rodada enquanto eu resolvia outra coisa.
Tempo estimado de leitura: 6 min. Você sai daqui sabendo o problema que me fez construir a skill orchestrating-terminal-agents e o que quebrou no caminho. Se você usa Claude Code todo dia e já quis soltar vários agentes ao mesmo tempo sem perder o controle, fica por aqui.
O Problema
Eu tenho uma startup pessoal, a Ondokai, que venho construindo há quase um ano. Lá dentro tem muita tarefa que dá pra paralelizar com Claude Code, e eu fazia isso na mão: criar a worktree, abrir o agente, esperar terminar, commitar quando ele não commitava, mergear de volta pra branch e ajustar conflito, às vezes na mão, às vezes com IA. Cada rodada dessas me arrancava do flow. Doloroso.
Rodar um agente de código é fácil. Rodar vários em paralelo, com isolamento real e sem perder o controle, não é. Dois agentes no mesmo working tree se atropelam, orquestrador headless esconde o que o agente tá fazendo, e empilhar tmux no caminho só adiciona camada.
(Parenteses pra quem nunca viu git worktree, disclaimer rápido: é um segundo working tree do mesmo repositório, com branch própria, sem clonar nada de novo. Dois agentes editam o mesmo projeto sem pisar um no pé do outro. Dito isso, seguimos.)
A Solução
Basicamente eu criei uma maneira, uma skill, do Claude Code criar hordas de agentes em abas do kitty e continuar o trabalho até o merge voltar. É uma Agent Skill e um par de CLIs independentes, então qualquer script (ou qualquer agente de IA num terminal) consegue usar. O repo: orchestrating-terminal-agents.
E aqui a parte que quase ninguém acredita: você não precisa entender o miolo. A skill foi escrita pra ser lida pela IA, não por você. Instala na máquina, referencia no prompt ("use a skill orchestrating-terminal-agents") e pronto, seja no Claude Code, Copilot, Gemini CLI, Kimi, Open Code ou pi coding agent.
# terminal: 3 agentes em plan mode, cada um numa worktree nova, prompt já submetido
fan 3 "refatore o módulo de auth" --repo ~/Projects/meu-app
# escolhendo tudo: instalação, modelo, mode, effort
fan 2 "..." --repo ~/Projects/meu-app --profile dsa --model 'opus[1m]' --permission-mode plan --effort max
# um prompt DIFERENTE por agente (o nº de prompts define o nº de agentes)
python3 scripts/fanout.py --repo ~/Projects/meu-app \
--prompt "cuide do auth" --prompt "cuide do cache" --prompt "cuide dos logs"
O fan é um alias fininho em cima do scripts/fanout.py. A primeira forma cobre quase todo o meu uso; as outras existem porque um dia eu precisei delas.
Funcionalidades
- Fan-out de verdade: N abas do kitty, cada uma com sua worktree (
<pai>/<repo>.worktrees/<branch>) e seu branch. Cada worktree gravabranch.<b>.origdire.origbranchno git config, então o fluxo de finalização sabe de onde ela saiu e pra onde volta no merge. - Um prompt por agente, se você quiser: um
--promptsó vira missão compartilhada; N prompts viram N missões diferentes na mesma chamada. Foi assim que mandei um cuidar do auth, outro do cache, outro dos logs. - Tudo por flag: instalação do Claude Code (
--profile, em cima deCLAUDE_CONFIG_DIR), modelo (opus,fable,sonnet,haiku,opus[1m]), permission mode (defaultplan) e effort (defaultmax). - Visível, não headless: cada agente roda numa aba que você vê. Dá pra ler o plano sendo escrito, digitar no meio, interromper com ESC e assumir a sessão na hora que quiser.
- Primitivas que dirigem qualquer TUI: o
kittyctl.pyabre abas, digita texto literal, manda tecla simbólica, espera a tela casar um regex e lê a tela de volta. Agente é só um dos casos de uso. - Yolo ligado por default: todo claude sobe com
--dangerously-skip-permissions, a menos que você passe--no-yolo. Defendo a escolha ali embaixo.
Stack Técnica
- "Frontend": as abas do kitty. A UI é o terminal mesmo, com user vars em cada janela pra matcher estável (
var:fan=<run_id>). - Backend: dois scripts de Python só com stdlib,
kittyctl.py(primitivas) efanout.py(a receita), falando com o remote control nativo do kitty (kitty @) através de socket Unix. Nada de tmux ou Zellij: uma camada a menos de RAM e redraw. - Infra:
git worktreepra isolamento, zsh pros aliases e pro wrapper de retry, Claude Code (testado na v2.1.218) resolvido porFANOUT_CLAUDE_BIN,PATHou~/.local/bin/claude. Desenvolvido em Pop!_OS 24.04 (X11), kitty 0.48, zero dependência de pip.
O único requisito de config é habilitar o remote control:
# ~/.config/kitty/kitty.conf
allow_remote_control socket-only # NUNCA 'yes': com yes, escape sequence impressa no terminal controla o kitty
listen_on unix:@mykitty
socket-only importa dobrado quando você roda agente de IA: com yes, um cat num arquivo malicioso poderia abrir abas e executar comandos. E reinicia o kitty depois de editar, porque instância já aberta não cria o socket.
O Desafio Mais Interessante
O bug mais desleal foi o exit code que mente. kitty @ send-key e send-text saem com exit 0 mesmo casando zero janelas. É arquitetural: disallow_responses no fonte do kitty. Ou seja, seu script digita pro void, recebe sucesso e segue a vida. Falha em silêncio, a pior espécie.
A saída foi trocar a confiança do envio pela confiança da tela. O kittyctl pré-checa todo matcher com ls e aborta em vez de mentir sucesso; depois do envio, quem confirma é o wait-for, que substitui sleep cego por regex com timeout:
# terminal: dirigindo um REPL, cada passo verificado pela tela
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
Repara na separação entre send-text e send-key enter. Isso é a regra 2 da skill: o Claude Code liga bracketed paste (DECSET 2004), e um \r no fim do mesmo send-text cai dentro do envelope de colagem em vez de submeter o prompt. Tecla especial segue a mesma lógica: sempre nome simbólico (shift+tab, escape), nunca escape cru, porque o kitty codifica conforme o modo que a TUI negociou.
Tem mais gotcha tratado no repo: o Shift+Tab que cicla os modos do Claude Code (3 teclas, e a ordem muda entre versões), o diálogo de confiança que trava toda worktree nova, o ambiente CLAUDE_* que vaza do agente pai pro filho. O LEARNINGS.md é a memória episódica de tudo isso.
O Que Aprendi
A orquestração boa é a que devolve atenção. Meu caso real: escrevi as tarefas no prompt principal, mandei o Claude usar a skill pra disparar as janelas e pedi pra ele só aguardar até todas as worktrees serem mergeadas de volta. Cada prompt filho já carregava resolução da tarefa, commit, push e merge back. Rodaram 10 agentes simultâneos, independentes, e os 10 voltaram. Eu coordenava com o Fable e tinha resposta automática configurada em até 5 minutos: se eu não respondia, ele pegava a recommendation e seguia. Melhor paralelização que eu já tive, de longe.
Sobre o yolo por default, minha defesa honesta: rode tudo dentro de um docker ou VM e o risco sai de cena. Dar permissão progressiva é uma ilusão, porque uma hora você para de ler os prompts. Você vira o gargalo. Pra mim, vale muito mais revisar o diff de uma branch grande no final do que clicar escolha de permissão o dia inteiro no meio de outras demandas. Um prompt bem estruturado e guardrails certos no claude.md seguram o agente melhor que o meu clique cansado.
E o trade-off de flags contra teclas: automação de TUI por injeção de tecla é frágil, cheia de race e tecla perdida em silêncio. Sempre que existe flag de CLI, a skill prefere flag (--permission-mode plan em vez de ciclar Shift+Tab). Tecla só quando não tem jeito, e aí com verificação de tela em cada passo. Chato de escrever uma vez. Confiável pra sempre.
Experimente!
# terminal: clone, link como skill global do Claude Code e 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> [opções]' >&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 "$@"; }
Instala, abre o Claude Code e pede: "use a skill orchestrating-terminal-agents pra disparar 3 agentes nesse repo". Começa com 2 ou 3, não com 10. Aí me conta.
Se der pau, abre uma issue com o que apareceu na tela, que o capture existe pra isso. E se a skill te economizar uma tarde de worktree manual, manda esse post no Slack ou no WhatsApp do time: alguém lá tá mergeando conflito na mão nesse exato momento.