A Window Per Agent Team

Skip the details — take me to the bottom line ↓

I have written about how my agents are organized and what they share. Both of those are about structure. This one is about the far more mundane question that structure raises the moment you have more than three of them:

how do you actually sit in front of them?

The answer is one iTerm2 window per team, with a tab per agent inside it — three windows in total, and I move between them the way you would move between three projects. That took about twenty lines of shell, and then one genuinely annoying afternoon.

The grouping is not a filing decision I made for the terminal. It is the same grouping as the org chart — the four agents that share a codebase come up together, the seven ops specialists come up together, and the independent ones come up together. The cockpit ended up mirroring the structure without my planning it, which in hindsight is the only arrangement that could have worked: the agents you want on screen at once are the ones that already talk to each other.

Control mode, which I had ignored for years

tmux has a mode I had always scrolled past in the man page: tmux -CC. Instead of drawing its own status bar and windows inside your terminal, it hands the structure to the terminal emulator over a control channel. iTerm2 understands that channel, and renders each tmux window as a native tab.

So tmux -CC attach gives you real tabs — real ⌘1, ⌘2, real tab bar, real scrollback — over a session that is still a tmux session, still detachable, still running on the box when you close the laptop.

That is the whole trick. Everything after this is plumbing.

A launcher per group, not per agent

Each agent already had a one-line script that started it in its own directory. What I wanted was one script per team: one tmux session, one window per agent, each running claude in that agent’s own directory.

There are three, and they are the three groups from the earlier posts — the shared-codebase four, the seven ops specialists, and the independent standalone agents. Membership is a list at the top of each script, so enrolling a new agent is adding one line. One of them also mixes in a couple of plain shell windows at $HOME, which is worth knowing: a launcher’s windows do not all have to be agents.

The guard that makes re-running safe

The first version rebuilt the session every time. Run it twice and you get a second claude in every directory, two of them fighting over the same conversation, plus a duplicate tab per agent.

So every launcher now opens with tmux has-session: if the session is already up, attach to it and stop. Re-running is idempotent — you land back where you left off. A fresh argument tears the old session down first and rebuilds clean.

It is four lines, and it converts a script you have to think before running into one you can mash blindly.

One switch, two ways to attach

The awkward part is that -CC is a client flag. It only works on attach, so it cannot be baked into the script that builds the session — and I wanted the same script to serve both a plain ssh from a terminal and iTerm2’s integration.

One environment variable settles it:

if [[ -n "${TMUX_CC:-}" ]]; then CC="-CC"; else CC=""; fi
...
exec tmux $CC attach-session -t "$SESSION"

Which makes the iTerm2 profile a single tidy line:

ssh -t user@host "TMUX_CC=1 ~/tmux-up"

Open that profile and I am looking at one team, one tab per agent, on a machine somewhere else. Swap the launcher path for a different profile and I get a different team. Drop TMUX_CC=1 and the identical script gives an ordinary terminal attach.

A cockpit that dies when you shut the laptop is not a cockpit

Plain ssh does not survive sleep. Close the lid, change networks, walk out of range — the connection drops, the -CC client dies with it, and the tabs vanish. The tmux session is still running happily on the box, which is the point of tmux, but you are back at a login prompt and have to go and find it again.

The reflex answer is mosh, which exists precisely for flaky links and roaming laptops. It does not work here. mosh is a terminal emulator in its own right — it predicts and re-renders the screen locally, which is what makes it feel instant over bad links — and control mode is not screen output. It is a protocol between tmux and iTerm2 that has to arrive intact. mosh has nothing to pass it through with.

EternalTerminal is the one that fits — and I did not find it. My buddy Nick Majeran introduced me to it years ago, long before I had a problem shaped like this one. It sat in the back of my head until the afternoon ssh kept dropping my tabs, and then it was obvious.

It is a durable transport that reconnects itself across sleep and IP changes, and it is transparent enough to carry the control channel. The two halves compose cleanly and the division of labour is worth stating, because it is what makes the whole arrangement work:

ET keeps the transport alive. tmux keeps the session alive.

Neither one is doing the other’s job. The laptop wakes, ET re-establishes the connection underneath, and the tmux -CC attach that was already running is simply still there — so iTerm2 drops back into the same native tabs, with each agent where you left it.

It survives an overnight sleep. I left it and went to bed rather than believing the design; the sessions were still there in the morning, tabs intact. One night is not a season, but it is the test that matters — the failure I was guarding against happens the first time the lid closes.

The nice part is how little had to change for it. The launchers are transport-agnostic — they build and attach a tmux session and have no opinion about how you reached the machine. Only the one line in the iTerm2 profile moved, and the old ssh form still works as a fallback. It just does not survive sleep.

et looks like ssh and is not a drop-in

The iTerm2 profile command becomes:

et user@host --command "TMUX_CC=1 ~/tmux-up"

Three things bit me, all because the muscle memory is ssh:

Drop -t. In ssh it forces a PTY. In et it is --tunnel, for port forwarding, so it misparses. ET always allocates a PTY anyway, so there is nothing to force.

The remote command is a flag, not a trailing argument. ssh host "cmd" becomes et host --command "cmd" — there is no positional slot for it.

Give the profile an absolute path to et. iTerm2 runs a profile command under a reduced launchd environment whose PATH does not include wherever your package manager installs things. Bare et is simply not found, with a message that sends you looking in the wrong place entirely. Point the profile at the full path — whatever your package manager put it at.

And the one that is not a flag at all: ET needs its server half running on the remote machine. Unlike ssh, which is already there on anything you would want to connect to, this is a daemon you install and have to make sure comes back after a reboot.

It does not need root, though, and that is the part worth knowing if you do not have any. etserver runs perfectly well as an ordinary user, started by a systemd user service — enable lingering for the account and it comes back at boot with nobody logged in. On a machine where you are a guest rather than an owner, that is the difference between this being available to you and not.

And then the tabs were useless

Here is the part worth reading, because I got it wrong twice.

Every tab was labelled with whatever its agent was currently doing. Not meta and blog and dns“Extract agent knowledge to meta directory”, and four more like it, each one truncated at about thirty characters. Claude rewrites that string every time its task changes, so the labels also churned while I watched.

A tab bar where every tab says what its agent is thinking about, and none say who it is, is worse than no tab bar at all.

The same window, before and after one setting. Both bars are correct renderings of a real tmux field — the bug was never that something was broken, only that iTerm2 was reading the wrong one.

Wrong fix number one: allow-rename off. The obvious suspect is tmux’s automatic window renaming, so I turned it off. It worked — the tmux window name stayed exactly as the launcher had set it. The tabs did not change at all, because the window name was never what iTerm2 was showing.

Wrong fix number two: iTerm2’s own setting. There is a checkbox, “Applications may change the title.” Turning it off did nothing either. In control mode the title does not arrive as an escape sequence for the emulator to interpret — it comes over the control channel, and sails straight past the setting that governs the other path.

Two plausible fixes, two hours, no progress. What broke it open was one command:

tmux list-windows -F '#{window_name} / #{pane_title}'

That prints, for every window, the two fields side by side. The window name was clean — meta, blog, exactly as assigned. The pane title was the churning task string. So the bug was not that anything was wrong. Both fields were correct. iTerm2 was reading the second one.

The actual fix, and the gotcha behind it

In control mode iTerm2 labels a tab from the tmux window title — which tmux emits only when set-titles is on. tmux’s default is off, so iTerm2 never received one and fell back to the pane title. Two lines:

set -g set-titles on
set -g set-titles-string '#W'

#W is the window name — the one the launcher assigns. Pair it with allow-rename off and automatic-rename off so nothing later overwrites it, and the tab bar reads like a staff directory.

You must detach and reattach — a bare detach will not do

iTerm2 reads set-titles only when the integration begins. Change it while attached and nothing happens, which is exactly the feedback you do not want while hunting a bug of this kind — a correct fix that looks like another failure.

And ~/.tmux.conf is read at server start, so if the server is still alive from another session, set it live too:

tmux set -g set-titles on \; set -g set-titles-string '#W'

I lost time to both of these. Between them they make a working fix look like a fifth idea that also did not work.

The thing worth carrying away

Every wrong turn here shared a shape: I assumed something was broken when nothing was. The window name was right. The pane title was right. iTerm2 was faithfully displaying a real field. The defect lived entirely in which of two correct values was being read — and no amount of staring at either value alone would ever have shown it.

Printing both fields next to each other took one command and ended the hunt immediately. That is the general lesson, and it is the same one I keep writing down about clocks: when something is confidently wrong, stop interrogating it and get a second reading beside the first. The disagreement is the information.

The short version

  • One window per team, not one per agent and not one for everything. The grouping that works is the one you already have — the agents you want on screen together are the ones that already talk to each other.
  • tmux -CC is the whole trick. Control mode hands the window structure to iTerm2, which renders it as native tabs over a session that is still detachable and still running when you close the laptop.
  • Guard the launcher with has-session so re-running attaches instead of building a second claude in every directory.
  • Tabs come from the tmux window title, which tmux only emits when set-titles is on. Without it iTerm2 falls back to the pane title, which Claude rewrites constantly. set-titles-string '#W', then detach and reattach — the setting is read only when the integration begins.
  • ssh does not survive sleep and mosh cannot carry control mode. Use a transport that can: ET keeps the connection alive, tmux keeps the session alive, and neither does the other’s job. The launchers do not care which you used — only the terminal profile changes.
  • Co-attaching is not merging. Every tab still runs in its own directory, so the agents’ isolation is exactly what it was.

One last thing, since it looks like the opposite

Sitting a team’s agents in one window makes them look like people in a room. They are not.

Sharing a tmux session is not sharing anything else. Each tab still runs claude in that agent’s own directory, so transcripts and memory key exactly as they did when each was launched alone. The isolation described in what a Claude army shares is completely untouched by co-attaching them — this is a cockpit, not a merger.

Which is a distinction I am glad is enforced by where the directories are rather than by my remembering it, because from where I sit it genuinely looks like one thing.

What a Claude Army Shares →