The tmux setup people build for parallel agents, and what it can't do

Claude Code will build the simple case for you: --worktree --tmux gives one agent an isolated worktree and a tmux session. For several agents you end up hand-building a session per project, a window per agent, send-keys dispatch, and a status line. That setup breaks on approvals, on telling working apart from finished, and on scraping panes.

The hand-built version is worth respect. Nobody told anyone to build it. People running Claude Code or Codex in three tabs hit the same wall at roughly the same time, reach for tmux, and land on nearly identical configs without ever reading each other’s dotfiles. Convergence like that usually means the shape is right.

The mechanics are covered properly in the tmux for AI coding agents guide. This piece is about the part that guide can’t fix.

start with the flag, because it exists

Before building anything by hand, use what ships. Claude Code has -w, --worktree [name], which creates a git worktree for the session, and --tmux, which creates a tmux session for that worktree.

claude --worktree api-v2 --tmux

One command, and an agent is running on its own branch in its own tmux session. Two things about how it behaves. --tmux requires --worktree, so it won’t wrap a checkout you already have. And it prefers iTerm2 native panes when they’re available, so on iTerm2 you get split panes rather than tmux windows; --tmux=classic forces traditional tmux when that’s what the rest of your tooling expects.

For one agent on one isolated branch, that’s the job done, and hand-rolling it is wasted effort. It also fixes the unit of organization in a specific way: the session belongs to a worktree, not to a project. Run it three times in one repo and you have three sessions, unrelated to each other, none of which knows the others exist.

Which is where the hand-built setup starts.

the setup everyone converges on

Everything from here is the multi-agent case: several agents visible at once, dispatch between them, and some way to know which one has stopped. It comes together in about four steps, in this order, every time.

One session per project. Not per agent. Projects are the unit you switch between, agents are the unit inside them.

tmux new-session -d -s crystl-web -c ~/Projects/crystl-web
tmux new-session -d -s api -c ~/Projects/api

One named window per agent. The name is doing real work here, because window index numbers shift when you close one and the name doesn’t.

tmux new-window -d -t crystl-web -n docs
tmux new-window -d -t crystl-web -n tests
tmux new-window -d -t crystl-web -n refactor

send-keys to start them. Once you can dispatch by name, a shell function replaces a lot of clicking.

tmux send-keys -t crystl-web:tests 'claude' Enter

A status line you built yourself. This is the step that turns a tmux config into a personal project. Some version of it exists in every setup:

tmux set -g status-interval 5
tmux set -g status-right '#(~/bin/agent-status)'
tmux setw -g monitor-silence 60
tmux set -g visual-silence on

monitor-silence 60 flags a window that has produced no output for a minute. It is the closest thing tmux has to “this agent might be done,” and it’s the reason so many of these configs look alike. Everyone finds it, everyone turns it on, everyone eventually notices it fires on an agent that is thinking hard and stays quiet on an agent that is stuck redrawing a spinner.

Add tmux attach -t crystl-web, a select-layout tiled for the moments you want to see everything, and that’s the setup. It is genuinely good. Sessions survive a closed terminal, an SSH drop, and a laptop lid. Nothing about it is a mistake.

where it stops working

Four specific failures, in the order most people hit them.

1. the approval that sat for twenty minutes

You alt-tabbed to a browser. An agent in crystl-web:refactor asked whether it could run rm -rf node_modules. It has been sitting on that prompt since. You find it when you next cycle windows.

tmux cannot help with this, and the reason is structural rather than a missing feature. A blocked approval and an agent thinking hard produce the same thing at the terminal layer: a pane that isn’t scrolling. tmux moves bytes between a process and a screen. It has no notion of “this process is waiting on a human,” because that notion doesn’t exist in the byte stream. monitor-silence is an approximation of an approximation, and it fires on both cases identically.

The cost isn’t the twenty minutes. It’s that after this happens twice you start cycling windows on a timer, which is the exact polling loop the tiled layout was supposed to save you from.

2. you can’t tell which pane is working and which is done

The natural instinct is to ask tmux what each pane is running:

tmux list-panes -a -F '#{session_name}:#{window_name} #{pane_current_command}'

What comes back is the foreground process name, which tells you how the CLI was packaged and nothing else. A natively compiled agent CLI reports its own name. A node-wrapped one reports node. Both report the same string whether the agent is mid-edit, waiting for you, or finished four minutes ago.

#{window_activity} is a timestamp of the last output, which sounds closer but isn’t. An agent streaming a long file read looks identical to an agent printing a spinner. Neither timestamp distinguishes “produced output because it’s working” from “produced output because it’s asking.”

So people fall back to reading the panes. Which leads to the third failure.

3. capture-pane breaks the first time output wraps differently

Scraping is the obvious move. Pull the last lines of each pane, grep for a prompt pattern, decide who needs attention:

tmux capture-pane -p -t crystl-web:refactor -S -30 | tail -5

This works on your machine, on your font, at your window width, for about a week.

Then you resize the window and every wrapped line lands in a different place, so your tail -5 catches half a sentence. You add -J to join wrapped lines, which fixes that case and creates another: now a wide code block collapses onto one enormous line and your grep matches inside a diff instead of at a prompt.

Then you hit the real one. Most agent CLIs run as full-screen TUIs on the alternate screen, and tmux is explicit about what that costs you: with the alternate screen in use, the history is not accessible. -S -30 doesn’t reach back thirty lines. You get the current frame, mid-redraw, with a spinner glyph in it, and whatever the agent said two minutes ago is gone.

You are now writing a screen-scraper against a redrawing TUI to recover state the process already knew and threw away. That’s the moment the setup stops being a config and starts being a maintenance burden.

4. it ends at the edge of one machine and one screen

tmux sessions are per-host. Agents on your laptop and agents on a build box are two separate attach commands and two separate status lines. And when you close the laptop, the sessions survive but you don’t hear from them, because there is no path from a tmux pane to a notification on your phone.

what would actually fix it

Every one of those four is the same missing thing: the agent’s state exists, and it never makes it out of the pane as data.

The agent knows it’s blocked on a permission decision. It knows the tool call it wants to run. It knows it finished. That’s structured information at the moment it happens, and by the time it reaches tmux it has been rendered into characters on a grid. Reconstructing it by scraping the grid is guessing at something that was certain one layer up.

Fixing it means reading agent state at the source, before it becomes pixels, and pushing it to you instead of making you poll for it. That is a different layer of the stack than a terminal multiplexer, which is why no amount of tmux config gets there.

where crystl comes in

crystl is a terminal built for that layer. It reads agent state through the agent’s own hooks rather than by scraping the screen, so the four failures above land differently.

A pending approval surfaces as a floating panel with the actual tool call in it, plus a notification and a color change on the session, whether or not you’re looking at that project. You don’t discover it twenty minutes later. Every session’s real status, working or waiting or done, sits in the crystal rail as state rather than as a guess, and the agent activity panel puts live activity and token spend for every agent in one view. Sessions are grouped by project the way tmux sessions are, and they survive a restart the same way, so the part of the tmux setup that was working is kept rather than replaced. Agents on a remote host come through the same interface as local ones, and approvals can reach your phone.

Support depth is honest here: the deepest surfacing, the approval panels, is Claude Code-first, Codex is partial, and other CLIs are supervised at the terminal level. That last tier is roughly what tmux gives you, so nothing gets worse.

If you want several agents coordinating rather than just several agents running, crystl quest puts a party of them in a shared chat with file isolation per agent, which is the multi-agent workflow tmux send-keys is usually a first draft of. Wiring that version by hand in tmux is covered in running an agent team in tmux.

where tmux still wins

Not everywhere, and it’s worth being straight about it.

On a remote box over SSH, tmux is still the answer. Detached sessions that survive a dropped connection are the whole reason it exists, and --tmux doesn’t change that, because the flag creates the session and then leaves. Same for a headless server, a CI box, or anywhere without a GUI. Same for scripted setup: new-session plus send-keys in a shell script gets a project’s whole agent layout running from a cold start, and --worktree --tmux covers the one-agent version of that in a single line.

The claim isn’t that tmux is the wrong tool, and it isn’t that the flag is a toy. Between them they solve the parts that were ever solvable at this layer. The rest of it, knowing which agent is parked and getting told about it, is a good solution to a problem tmux was never given the information to solve, and after the third rewrite of your capture-pane parser it’s worth asking whether the information should be coming from somewhere else.

crystl is free. Sign up at crystl.dev/login.