Back to articles
CategoryDemos

How I run 10 Claude Code agents in parallel without tmux or headless

July 24, 2026
Tags
Artificial IntelligenceLLMAutomationProductivityOpen Source
How I run 10 Claude Code agents in parallel without tmux or headless

Hero

In the opening GIF: ten kitty tabs, that terminal with the cat icon, each one running a Claude Code in plan mode inside its own git worktree. One command fired it all. I sat there watching the plans come to life in parallel, one per tab, with Fable coordinating the round while I sorted out something else.

Estimated reading time: 6 min. You'll walk away knowing the problem that made me build the orchestrating-terminal-agents skill and what broke along the way. If you use Claude Code every day and have ever wanted to unleash a bunch of agents at once without losing control, stick around.

The Problem

I have a personal startup, Ondokai, that I've been building for almost a year. There's plenty of work in there that can be parallelized with Claude Code, and I used to do it by hand: create the worktree, open the agent, wait for it to finish, commit when it didn't, merge back into the branch and fix conflicts, sometimes by hand, sometimes with AI. Every one of those rounds yanked me out of the flow. Painful.

Running one coding agent is easy. Running several in parallel, with real isolation and without losing control, isn't. Two agents in the same working tree trip over each other, a headless orchestrator hides what the agent is doing, and stacking tmux in the middle just adds one more layer.

(Quick aside for anyone who's never met git worktree, fast disclaimer: it's a second working tree of the same repository, with its own branch, without cloning anything again. Two agents edit the same project without stepping on each other's toes. That said, we move on.)

The Solution

Basically I created a way, a skill, for Claude Code to spawn hordes of agents in kitty tabs and keep working until the merge comes back. It's an Agent Skill and a pair of standalone CLIs, so any script (or any AI agent in a terminal) can use it. The repo: orchestrating-terminal-agents.

And here's the part almost nobody believes: you don't need to understand the guts. The skill was written to be read by the AI, not by you. Install it on the machine, reference it in the prompt ("use the orchestrating-terminal-agents skill") and done, in Claude Code, Copilot, Gemini CLI, Kimi, Open Code, or pi coding agent.

bash
# terminal: 3 agents in plan mode, each in a fresh worktree, prompt already submitted
fan 3 "refactor the auth module" --repo ~/Projects/my-app

# picking everything: installation, model, mode, effort
fan 2 "..." --repo ~/Projects/my-app --profile dsa --model 'opus[1m]' --permission-mode plan --effort max

# a DIFFERENT prompt per agent (the number of prompts sets the number of agents)
python3 scripts/fanout.py --repo ~/Projects/my-app \
  --prompt "handle auth" --prompt "handle cache" --prompt "handle logs"

fan is a thin alias over scripts/fanout.py. The first form covers almost all of my usage; the others exist because one day I needed them.

Features

  1. Real fan-out: N kitty tabs, each with its own worktree (<parent>/<repo>.worktrees/<branch>) and its own branch. Each worktree records branch.<b>.origdir and .origbranch in git config, so the finishing flow knows where it came from and where it merges back to.
  2. One prompt per agent, if you want: a single --prompt becomes a shared mission; N prompts become N different missions in the same call. That's how I sent one to handle auth, another cache, another logs.
  3. Everything via flag: Claude Code installation (--profile, on top of CLAUDE_CONFIG_DIR), model (opus, fable, sonnet, haiku, opus[1m]), permission mode (default plan) and effort (default max).
  4. Visible, not headless: each agent runs in a tab you can see. You can read the plan as it's being written, type in the middle, interrupt with ESC, and take over the session whenever you want.
  5. Primitives that drive any TUI: kittyctl.py opens tabs, types literal text, sends symbolic keys, waits for the screen to match a regex, and reads the screen back. An agent is just one of the use cases.
  6. Yolo on by default: every claude comes up with --dangerously-skip-permissions, unless you pass --no-yolo. I defend that choice down below.

Tech Stack

  • "Frontend": the kitty tabs. The UI is the terminal itself, with user vars on each window for a stable matcher (var:fan=<run_id>).
  • Backend: two Python scripts with stdlib only, kittyctl.py (the primitives) and fanout.py (the recipe), talking to kitty's native remote control (kitty @) over a Unix socket. No tmux or Zellij: one less layer of RAM and redraw.
  • Infra: git worktree for isolation, zsh for the aliases and the retry wrapper, Claude Code (tested on v2.1.218) resolved via FANOUT_CLAUDE_BIN, PATH, or ~/.local/bin/claude. Built on Pop!_OS 24.04 (X11), kitty 0.48, zero pip dependencies.

The only config requirement is enabling remote control:

ini
# ~/.config/kitty/kitty.conf
allow_remote_control socket-only   # NEVER 'yes': with yes, an escape sequence printed to the terminal controls kitty
listen_on            unix:@mykitty

socket-only matters twice as much when you run AI agents: with yes, a cat on a malicious file could open tabs and run commands. And restart kitty after editing, because an instance that's already open won't create the socket.

The Most Interesting Challenge

The sneakiest bug was the exit code that lies. kitty @ send-key and send-text come out with exit 0 even when they match zero windows. It's architectural: disallow_responses in kitty's source. Meaning your script types into the void, gets a success, and moves on with its life. Silent failure, the worst kind.

The way out was trading trust in the send for trust in the screen. kittyctl pre-checks every matcher with ls and aborts instead of lying about success; after the send, the confirmation comes from wait-for, which replaces blind sleeps with a regex plus a timeout:

bash
# terminal: driving a REPL, every step verified by the screen
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

Notice the split between send-text and send-key enter. That's rule 2 of the skill: Claude Code turns on bracketed paste (DECSET 2004), and a \r at the end of the same send-text lands inside the paste envelope instead of submitting the prompt. Special keys follow the same logic: always a symbolic name (shift+tab, escape), never a raw escape, because kitty encodes according to the mode the TUI negotiated.

There are more gotchas handled in the repo: the Shift+Tab that cycles Claude Code's modes (3 keys, and the order changes between versions), the trust dialog that hangs every fresh worktree, the CLAUDE_* environment that leaks from the parent agent into the child. LEARNINGS.md is the episodic memory of all of that.

What I Learned

Good orchestration is the kind that gives your attention back. My real case: I wrote the tasks in the main prompt, told Claude to use the skill to fire the windows, and asked it to just wait until every worktree got merged back. Each child prompt already carried task resolution, commit, push, and merge back. 10 agents ran at the same time, independent, and all 10 came back. I was coordinating with Fable and had auto-reply set to kick in within 5 minutes: if I didn't answer, it took the recommendation and kept going. Best parallelization I've ever had, by far.

On yolo by default, my honest defense: run everything inside a docker container or a VM and the risk leaves the scene. Granting permissions progressively is an illusion, because at some point you stop reading the prompts. You become the bottleneck. To me, reviewing the diff of a big branch at the end beats clicking permission choices all day long in the middle of other demands. A well-structured prompt and the right guardrails in claude.md hold the agent better than my tired clicking.

And the flags-versus-keys trade-off: TUI automation through key injection is fragile, full of races and keys lost in silence. Whenever a CLI flag exists, the skill prefers the flag (--permission-mode plan instead of cycling Shift+Tab). Keys only when there's no other way, and even then with screen verification at every step. Annoying to write once. Reliable forever.

Try It!

bash
# terminal: clone, link as a global Claude Code skill, and 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 'usage: fan <N> <prompt> [options]' >&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 "$@"; }

Install it, open Claude Code, and ask: "use the orchestrating-terminal-agents skill to fire up 3 agents on this repo". Start with 2 or 3, not 10. Then come tell me how it went.

If it blows up, open an issue with whatever showed up on the screen, that's what capture is for. And if the skill saves you an afternoon of manual worktrees, send this post to the team Slack or WhatsApp: someone in there is merging conflicts by hand right this minute.