How do you get AI agents to talk to each other?

Four options, in the order most people try them: a shared file both agents read and write, a git branch one pushes and the other pulls, tmux send-keys typing from one agent’s shell into another’s input box, and a coordination layer built for the job. The first three all work. None of the first three can tell you a message arrived.

That gap is the whole subject. Getting text from agent A to agent B is easy. Knowing that B received it, read it, acted on it, and isn’t currently stuck waiting on an approval prompt is the hard part, and it’s the part every DIY setup skips.

This post is for the person with two agents open right now that can’t see each other. If you’re evaluating or implementing a wire protocol instead, you want agent communication protocols: A2A, ACP, and MCP, which is a different page for a different reader.

What you actually want

Nobody wants “agent to agent communication” in the abstract. In practice there are three jobs:

  1. Handoff. Agent A finishes, agent B picks up where it left off.
  2. Lookup. Agent B needs something agent A already discovered, like the shape of an API that A just built.
  3. Status. You, or a supervising agent, need to know that A is done, or blocked, or waiting on you.

Each option below does some of these and not others. Pick by which one you need.

Option 1: a shared file

The cheapest thing that works. Both agents read and write one file in the repo.

# agent A, on finishing
cat >> .agents/outbox/frontend.md <<'EOF'
## 2026-07-31T14:02Z
session endpoint is live: POST /api/session, body {email, token}.
returns 401 {error} on a bad token. safe to wire the client.
EOF

Then you tell the other agent it exists, in CLAUDE.md or AGENTS.md so it survives a restart:

## Coordination
Before starting a task, read every file in `.agents/outbox/`.
When you finish, append your findings to `.agents/outbox/<your-name>.md`.

Two notes on making this less bad. Give each agent its own file rather than a shared one, because agents don’t append with >>, they open the file in an editor tool, read it whole, and write it back whole. Two agents doing that on the same file at the same time means one silently loses the other’s message. One file per writer removes the race entirely.

The second note is the one you can’t fix. Nothing makes the other agent read the file. An agent mid-turn is not watching your filesystem. It reads on its next turn, if the instruction survived its context, and if it feels like it. By then it may have already redone the work A just described. A shared file is a bulletin board, not a message.

Good for: durable facts that stay true (ports, schemas, conventions). Bad for: anything time sensitive.

Option 2: hand off through a branch

Agent A commits and pushes, agent B pulls. This is a real handoff and it has properties the others don’t: it’s durable, reviewable, and git already knows how to merge two people’s changes.

# agent A
git commit -am "auth: add POST /api/session" && git push -u origin feat/auth

# agent B, in its own worktree
git fetch && git merge origin/feat/auth

The cost is granularity and bandwidth. You only communicate at commit boundaries, so “I’m about to change the response shape, don’t start yet” has nowhere to live. Commit messages are a thin channel for intent, and a blocked agent has nothing to commit at all.

If your agents are sharing a repo, get the isolation right before you worry about the messaging: see two agents on the same repo and git worktrees for parallel development.

Good for: sequential handoff where the work itself is the message. Bad for: status, questions, anything before the work exists.

Option 3: tmux send-keys

This is the one people underrate. If each agent runs in a tmux pane, any shell on the machine can type into any other pane, which means one agent can put text directly into another agent’s input box:

tmux send-keys -t api:main -l "frontend here: session endpoint is POST /api/session"
sleep 0.3
tmux send-keys -t api:main Enter

That sleep is not superstition. Agent input boxes re-render on paste and an Enter arriving in the same frame gets swallowed. The full set of traps, including why Enter and Space in your prompt text get sent as keypresses, is in tmux for AI coding agents.

What makes this different from options 1 and 2: the message becomes an actual turn in the receiving agent’s conversation. It can’t be ignored the way a file can. The receiving agent will respond to it, because from its side a human just typed.

The traps are the flip side of the same fact. send-keys types into whatever the target pane is showing. If that agent is sitting on a “do you want to allow this edit?” prompt, your carefully composed status update becomes the answer to that prompt. If it’s mid-turn, your text sits in the input box until it finishes, arriving with no relationship to when you sent it. And you still have no idea whether the agent understood any of it, because the only way to find out is capture-pane and reading the screen yourself.

For the full wiring of a tmux agent team, panes per role, a dispatch script, and the polling loop that tries to detect idle, see running an agent team in tmux.

Good for: real delivery into a live session. Bad for: knowing what happened next.

The four guarantees none of these have

Line the options up and the same holes appear in all of them.

Delivery. Did the message land as something the other agent will actually process, or is it sitting in a file nobody opened? Only send-keys gets close, and only if the pane was in the right state.

Receipt. Did the agent read it and take it in? There is no acknowledgement anywhere in options 1 through 3. You find out by reading the other agent’s transcript yourself, which is the manual work you were trying to remove.

Blocked detection. A quiet agent is either thinking, finished, or parked on an approval prompt waiting for a human. Those look identical from outside. A polling loop over capture-pane output can guess at it with a heuristic on the last few lines, and the heuristic breaks the moment the agent prints something that looks like a prompt. Getting this wrong is expensive in a specific way: a blocked agent burns the entire wall-clock time you thought it was working.

Addressing. “The backend agent” is a pane ID until you write down what it is. Next week, after a reboot, it’s a different pane ID. Every DIY setup grows a registry file eventually.

You can build all four. People do. It’s a weekend for the first version and then a long tail of the heuristics being subtly wrong, which is exactly the kind of thing you don’t want load-bearing under a team of agents changing your code.

What a coordination layer adds

This is the problem crystl was built around, so here is what it does concretely rather than in the abstract.

The crystl CLI is available to every agent running inside crystl, which means an agent can inspect and drive its siblings without you in the middle:

crystl shards --gem my-app                        # who's running, and who's blocked
crystl screen --gem my-app --shard api            # read another agent's terminal
crystl send --gem my-app --shard api "..." --wait # deliver, with a receipt
crystl notify --done --shard lead "auth merged"   # tell the supervisor you finished
crystl wait done --timeout 600                    # block on an event, don't poll

The --wait on send is the delivery guarantee that send-keys lacks: it confirms the text landed in the target’s session, either as a turn in its transcript or in the agent’s own input queue behind the turn it’s already running, not as an answer to whatever prompt happened to be on screen. And when there’s no receipt, the exit code says whether the target was busy (exit 3, it’s queued, don’t re-send) or idle (exit 1, re-sending is safe). crystl shards flags a parked agent with awaiting input rather than leaving you to infer it from silence, which closes the blocked-detection hole. And shards have names you chose, so addressing survives a restart.

For agents that need to coordinate continuously rather than exchange occasional messages, crystl quest puts a party of agents in one shared chat where they @mention each other, DM, and broadcast decisions to everyone at once. The design argument for that shape, versus the orchestrator-with-subagents shape most tools ship, is in roundtable agent orchestration, and the practical setup including open versus sealed mode is in multi-agent development with crystl quest.

If what you want is closer to “farm out a task list and tell me when it’s done” than a live conversation, Fanout is the lighter option: one session becomes the manager, spawns a worker per task, handles their questions, and merges the results.

Picking one

  • Facts that stay true, and you’re fine with the other agent reading them eventually: shared file. Put the instruction in AGENTS.md and move on.
  • Sequential handoff where the code is the message: a branch. It’s durable and reviewable and you already know how to merge it.
  • You need the message to land in a live session right now, and you’re happy to check the result yourself: send-keys.
  • More than two agents, or you’ve started writing a polling loop to guess whether one is stuck: stop building the loop. That’s the point where the missing guarantees start costing more than the tooling.

The honest summary is that agent to agent communication is a solved problem for delivering text and an unsolved one for everything after delivery. Whatever you build, build the “is it blocked?” answer first.

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