Channels (Messaging)

MultiTerminal delivers agent-to-agent messages through Claude Code's native Channels transport. Each terminal runs a small channel server that pushes incoming messages straight into the session as structured <channel> events — replacing the older [cm] "check messages" nudge. Channels need a one-time authorization flag, which MultiTerminal applies for you.

What channels are

A channel is a first-class Claude Code transport for pushing messages into a running session. When another agent (or the desktop) sends you a message, it arrives inline as a structured event:

<channel source="multiterminal" from="Diana" priority="normal">
  Can you take the API page? I'll start on presence.
</channel>

You react to it immediately and reply with the channel's tools — no polling, no terminal-injected reminders. MultiTerminal provides a channel server (bundled in the plugin) so every docked terminal participates automatically.

How a message flows

  1. Each terminal's channel server starts up and registers its listen port with the MultiTerminal broker (POST /api/messaging/register with its agent name + channelPort).
  2. An agent calls send_message (or a skill does). The broker looks up the recipient's registered channelPort.
  3. The broker delivers the message over HTTP to that port.
  4. The recipient's channel server pushes it into the Claude Code session as a <channel> event.
  5. The receiving agent reads it and replies with the channel reply / send tools, which route back through the broker.

The channel server

The server is bundled with the plugin at server/multiterminal-channel.mjs and declared in the plugin's plugin.json:

"mcpServers": {
  "multiterminal-channel": {
    "command": "node",
    "args": ["${CLAUDE_PLUGIN_ROOT}/server/multiterminal-channel.mjs"]
  }
},
"channels": [
  { "server": "multiterminal-channel" }
]

It runs as an MCP server (stdio) for the session, and in parallel opens a small HTTP listener that the broker delivers messages to. If its preferred port is busy it walks forward through the allowed range (8800–8899) until it finds a free one, then re-registers the actual port with the broker, so multiple terminals on one machine don't collide.

The same channel also carries tool-permission prompts to the phone: when Claude needs approval to run a tool, the server forwards the request to your phone (via Multi-Connect) so you can answer remotely — reply yes, no, or always with the short request id to approve, deny, or auto-approve that tool for the session.

Enabling channels (the required setting)

The channel server ships inside the plugin rather than from a published marketplace, so Claude Code treats it as a development (inline) channel. Development channels must be explicitly authorized — otherwise Claude Code will not load the channel server. MultiTerminal launches every terminal with two flags:

# MultiTerminal applies these automatically when it spawns a terminal
claude \
  --plugin-dir <path-to-multiterminal-plugin> \
  --dangerously-load-development-channels plugin:multiterminal@inline
  • --plugin-dir loads the MultiTerminal plugin (hooks, skills, agents, and the channel server) for that session.
  • --dangerously-load-development-channels plugin:multiterminal@inline authorizes the plugin's inline channel server. (Plugins loaded via --plugin-dir are sourced as name@inline.)

For MultiTerminal terminals this is automatic — the launch builder adds both flags, so channels just work in any docked terminal. You only need to pass the flags yourself if you run Claude Code by hand (outside MultiTerminal) and want the same channel messaging.

The “plugin not installed” warning is a false positive

Every MultiTerminal terminal opens with two lines, and the second one looks like an error:

Channels (experimental) messages from plugin:multiterminal@inline inject directly in this session
plugin:multiterminal@inline · plugin not installed

The first line is honest. The second is true about the registry and misleading about MultiTerminal: Claude Code validates the channel entry against the list of installed plugins, and a plugin loaded with --plugin-dir is session-loaded rather than installed, so it is legitimately absent from that list. Channels are working; nothing is broken.

⚠️ Do not “fix” it by changing @inline to @multiterminal-marketplace. That silently kills channel messaging. Registration runs a different check from the warning, and a marketplace mismatch is skipped before the development-channel flag is considered — so --dangerously-load-development-channels does not rescue it. Nothing throws and nothing logs an error. Polling (get_messages) keeps working perfectly, so the only symptom is that pushed messages stop arriving, which reads as “nobody messaged me” rather than as a fault.

Switching to the installed plugin instead (enabling it at user scope and dropping --plugin-dir) does not help either: the installed copy lives in a separate cache directory that is a snapshot taken at install time, and it does not contain the channel server at all.

Since ticket 0ff1b520 (item 16) MT no longer launches with this flag at all; MultiTerminal.Tests/LaunchFlagContractTests.cs pins that no launch path emits it.

Environment variables

When MultiTerminal spawns a terminal it injects two of these — MULTITERMINAL_NAME and CHANNEL_PORT — so the channel server knows who it is and which port to listen on. The other two are not set by MultiTerminal; the channel server falls back to its own defaults:

VariablePurposeSet by MT?Default
MULTITERMINAL_NAMEThis terminal's agent name (e.g. Alice)Yesunknown
CHANNEL_PORTHTTP port the channel server listens on for incoming messages (MultiTerminal assigns a unique one per terminal, within 8800–8899)Yes8800
MT_API_URLMultiTerminal REST API base URL used for registration/routingNo — uses defaulthttp://localhost:5050
MULTITERMINAL_IDIntended for send_message routing, but effectively unused — the broker resolves terminals by agent name, so the server sends under MULTITERMINAL_NAME insteadNo — uses default(empty)

So in practice only the agent name and the channel port come from the launcher. MT_API_URL defaults to the local REST API, and the empty MULTITERMINAL_ID is harmless because routing is name-based.

Sending & replying

From an agent's point of view, channel messaging is just tools and events:

  • Receive: messages arrive as <channel source="multiterminal" from="…"> events — treat them the same as any teammate message and act on them.
  • Reply: use the channel reply tool (answer the sender) or send tool (message any agent by name).
  • Broker tools: the broader MCP surface (send_message, broadcast_message, get_messages) still works and routes through the same broker. See MCP Tools.

Legacy: the [cm] nudge

Before channels, MultiTerminal surfaced inter-terminal messages with a [cm] ("check messages") nudge — a string injected into the terminal (originally via ConPTY) that prompted the agent to poll get_messages. That approach was indirect and easy to miss. Channels replace it with direct, structured delivery, so [cm] is now deprecated. You may still see references to the inbox-check hook (see Hooks) for backward compatibility, but channels are the current mechanism.