# How Claude, Codex and Grok agents talk to each other through plain tmux

**Topic:** agent orchestration · **Published:** October 2026 · **Reading time:** 10 min

I run a team of AI agents on a regulated crypto platform. The agents come from different vendors. Some run on Claude Code, some on Codex, some on Grok. One Grok pane can run an open-weights model served through Ollama.

They do not share an API, a protocol or a message queue. They share a terminal multiplexer. Almost every message between them goes through plain tmux, driven by one shell script of about 870 lines.

This article explains why I chose that, how a message travels, which rules reduced the chaos, and what broke along the way.

## The problem: each vendor's agents only talk inside their own tool

Every vendor ships its own way to run sub-agents. Those sub-agents talk to their parent. They do not talk to an agent from another vendor.

I wanted a Claude lead to hand work to a Codex builder, and a reviewer on a third model to answer the lead. I also wanted a person to watch all of it. No vendor feature did that.

## Why tmux

Every agent I use is a command-line program. It reads text from a terminal and prints text back. So each agent already has an input box, and tmux can type into any input box.

That gives me three things for free:

- **No integration per vendor.** A new model joins the team when it runs in a terminal pane.
- **The human sees everything.** I can attach to any pane and read what the agents read.
- **One address scheme.** Each agent runs in its own tmux session, which I call a lane. The session name is the address.

The cost is that I type into a user interface, not an API. That is where most of this article's problems come from.

## How a message is sent

The obvious command is `tmux send-keys -t <lane> "message" Enter`. It fails in practice. If the target agent is busy, the Enter does not register, and the message sits unsent in the input box.

So I had my agents build a send script, and I set its rules. The call looks like this:

```
tmux-send.sh <lane> "<sender>: <message>"
```

The script does the following:

1. **Locks the target.** It takes one lock per target lane, so two senders cannot type into the same pane at once.
2. **Checks the pane.** It reads the screen and finds the real input box. It must recognise the frame, meaning the border and footer each tool draws around its input box, of a Claude, Codex or Grok pane. If it cannot read the frame, it refuses to send.
3. **Pastes once.** It loads the message as a single paste, so multi-line text arrives as one block.
4. **Presses Enter, then verifies.** It looks at the box again. It reports success only when it saw its own text in the box, pressed Enter, and then saw the box empty.
5. **Never re-pastes.** Re-checking is fine. Pasting a second time is not. A re-paste turns one message into garbage.

When the script cannot prove delivery, it exits with a "held" status and a machine-readable line. The caller can tell "nothing was pasted" from "already pasted" from "Enter already pressed". That distinction came from a bad incident: on 9 September 2026 a dispatcher retried one handoff 166 times because it only saw a non-zero exit code.

### Reading the pane back

The script verifies delivery, but I added a human rule on top. After every send, the sender reads the target pane back. If its whole message is still in the input box, the sender presses Enter and reads again. A message in a box is neither delivered nor discarded. The target may submit it later, out of context.

### Ghost text is not input

The Claude interface shows autocomplete suggestions and the empty-box placeholder in dim grey. Real input uses the default colour. A plain screen capture strips colour, so an early version of the script could not tell a suggestion from a message. It reported "still in box" when nothing was typed, and "sent" when it was not.

The fix reads the screen with colour codes, drops the dim runs, and counts only real text after the prompt. A long paste that the interface collapses to a short "pasted text" token also counts as real input.

## How agents know who is who

Each message starts with the sender's lane name and a colon, for example `reviewer: ...` or `lead: ...`. This is the signature.

The signature does one more job. It tells the system whose text is sitting in a box:

- **Signed text is agent residue.** If a sender finds a signed line left in another lane's box, it clears the line, records it in a residue log, and sends its own message. A test in the script's suite checks exactly this path.
- **Unsigned text may be mine.** I never sign my own typing. So the script keeps unsigned text and does not send over it.
- **A dead sender's signature is kept.** If the signature belongs to a session that is not live, the script keeps the text.

A session registry lists each lane with its purpose, model and account. It is a snapshot, not proof of delivery or ticket ownership. The rule is to re-read tmux before acting on it.

One more lesson: a desktop app session has no tmux lane. It must not take a lane name from tmux, because tmux then reports another lane's name. Such a session signs with a short id of its own and asks tmux lanes to reply in a named file, because it has no pane.

## Coordination notes

Messages are for short pokes. State goes in a file. Each day has one coordination file. Agents append dated, signed notes: what they changed, what they verified, what they will not touch.

A recent example: two sessions edited the same script within a minute, with no claim on the file. One of them wrote the collision into the day's note, stopped editing, and recorded the file checksums. I decided to keep both. The note gave me one place to see both edits and decide.

## Rules that reduced the chaos

Plain text between agents needs rules. These are the ones I kept:

- **One writer per task.** Other agents' work and claims are preserved.
- **A pause from the human beats any agent message.** An agent never infers permission to resume from another agent's note.
- **Every agent message ends in an action.** Clear the box or send it. No lane may leave text hanging in another lane's box. I set this rule after an agent left text in a peer's input box and I had to say so more than once.

## What broke, and how it was fixed

The script's backup files read like a changelog. Each backup carries a date and a reason.

- **Frame detection.** The script must recognise each pane layout, or it refuses to send. Several real incidents were refusals, not bad pastes:
  - A conversation renamed from outside the lane shows a custom title on its border, not the lane name. Every handoff to the lead was held until the parser accepted that title.
  - The model row can also show a folder label, a branch suffix or a label with punctuation. Each new form held some lane until I widened the parser.
  - The footer can grow a task roster and monitor counts. The parser now requires a known structure and refuses unknown trailing text.
  - Grok panes use a rounded box, not a rule frame. The first version read them as unreadable.
- **Process checks.** The script reads the target's command line and config directory to confirm what runs in the pane. It works on both Linux and macOS.
- **Grok queues.** A Grok agent that is mid-turn queues a prompt and does not always submit it. For that case I address the Grok conversation by session id, and the script holds if delivery cannot be proven. It never falls back to a pane paste. After a queued send, check that it landed when the turn ends. If it did not, press Enter once and verify.

The test suite drives a fake agent in a real tmux server. It has 26 tests. They cover a fresh send (one paste, one Enter), a foreign draft that must stay, a busy pane, ghost text, wrong-lane refusal and unknown footers.

## Limits and honest trade-offs

- **It types into a user interface.** A vendor update to the layout can break frame detection. A new agent-roster row in one update held every lane that had helpers running. The script refuses to guess. That is safe, but it means a lane can go unreachable until I update the parser.
- **Delivery is verified, understanding is not.** The script proves text arrived. It cannot prove the agent read it.
- **I tried a mailbox.** On 2 October 2026 I built a lane-to-lane mailbox on SQLite and Claude hooks, with 34 tests. Unread mail arrives as context on the next prompt. A wake sends only the word `inbox` through the same tmux script. It ran as a pilot on two lanes. Hooks only exist for tools that support them, so the plain tmux path stays the fallback.

## What I would do differently

1. **Write the signature rule on day one.** I added it only after agents kept mistaking each other's leftover text for my drafts.
2. **Test against real frames first.** I found most pane layouts in live use.
3. **Return a machine-readable outcome from the start.** The 166 retries came from a bare exit code.
4. **Keep state in files, not in boxes.** Boxes are for pokes.

The main idea is simple. A terminal is the one interface every agent from every vendor already has. If I build on it, a new model does not need an adapter. It needs a pane, a name and the rules above.

Want agents from different vendors working together in your setup? Email [hello@jeffkent.dev](mailto:hello@jeffkent.dev).
