Messaging
To hand work to an agent that is already running, you switch to its pane and type, or you script the same keystrokes: tmux send-keys -t %3 'rebase on main first' Enter.
To hand work to an agent that is already running, you switch to its pane and type, or you script the same keystrokes: tmux send-keys -t %3 'rebase on main first' Enter. That works, and it leaves two gaps. You have to know which pane holds which agent, and the text arrives the moment you send it, landing in the middle of whatever the agent was doing.
rimz message writes into the same pane through the same primitive and closes both gaps. You address the agent by handle, and you choose when the text lands: when its current turn ends, right now, or no earlier than a time you name.
Every send becomes a durable record before a byte reaches the pane. A busy agent, a room you close and reopen, or a failed write all leave a record you can read back, with the outcome on it, so nothing you send disappears quietly.
rimz message @claude "add coverage for the expiry edge cases" # parks: lands when @claude's turn ends
rimz message --steer @claude "stop: the parser test comes first" # writes into the live turn now
rimz message --schedule 60m @codex#feat-a "run the smoke test" # no earlier than an hour from now
rimz message @coder --wait "did the migration land? one line" # ask, and print the reply
rimz message @all "freeze new work; I'm cutting a release" # everyone in your channelThe same command serves you, your scripts, and the agents themselves: a running agent hands work to a teammate with the line you would have typed. rimz msg is a shorter spelling of it, and of every subcommand below.
If you leave the target off a send, RimZ refuses it and shows the command to use instead. Bare rimz message still reads your lane's messages; put inbox flags after list, as in rimz message list --json, rather than on the send form.
When an agent messages a peer it launched, the new turn reports its result back automatically. A steer into an already-running turn opens no separate report. Text you type during a launcher-requested turn becomes part of that turn's final answer; if the peer was waiting for its own children, your turn can finish that pending request too. A provider that omits prompt text from its turn-start hook cannot start a new report from that message; use --wait for a message-correlated reply instead.
Address an agent
An address is @handle[#channel]. The handle names who; the optional #channel names which group of agents. Leave the channel off and RimZ uses the one your pane sits in.
A handle resolves through the shortest unique name for a running agent:
@codexis a kind. It reaches the one Codex in your channel, and is ambiguous only when several share it.@planneris a profile or team role you defined in Markdown, so it reaches the right member wherever its pane sits.@swift-otteris the pet name listings show when a kind is not unique enough. The ordinal@codex-2remains typeable too; either names one specific instance.@allis every agent in the channel, minus the caller when an agent sends the message.
The @ sigil is required, so a stray word never reaches an agent: rimz message coder "…" fails with agent target `coder` must start with `@` (try `@coder`) rather than guessing. A raw pane id (tmux:%1, zellij:terminal_3) is the one address that needs no sigil. Handles are minted when an agent launches, and the agents guide covers how kinds, profiles, and team roles become them.
Reach across channels with #channel. @codex reaches the Codex in your current channel; @codex#feat-a reaches the one working the feat-a worktree from anywhere in the workspace. Use rimz message @codex#main "…" to reach the project's root channel. An agent keeps its card's channel even when it runs a command from the main checkout, so changing directory does not accidentally address another lane. The flags --channel <name> and --worktree <name> are the same restriction in flag form, and conflict with each other.
Channels
A channel groups the agents working one line of work inside a room. The sidebar groups its cards by channel, an address targets it with #channel, and RimZ reopens it as its own Zellij tab or tmux window when it restores the room. Your pane always sits in one, which is why a bare @codex scopes to it and you only reach across when you name another.
A channel is never created or removed on its own: it is derived from what backs it, and it exists for as long as that does. A #name comes from one of four places:
- A Git worktree. Every agent working an isolated worktree shares that channel, named for the worktree (worktrees guide).
- An explicit
--channel. A launch with--channel designputs its agents in#design, a lane with no branch and no directory behind it. It lasts while agents run in it. - A team in place. A named team launched at the room root gets a channel of its own,
<dir>/<team>. - The working directory. Agents with no other channel fall back to their directory's name, except at the project root, which always displays
main. The project's directory name remains accepted as an alias for#main.
rimz agents claude --channel design "draft it" # launch straight into #design, in a tab of that name
rimz message @planner#design --create "plan it" # reach into it; --create launches one if none is there
rimz agents list --all # every lane that has agents, with who is in each
rimz worktree list # every worktree lane, with or without agentsA --channel name takes ASCII letters, numbers, _, and -. Channels and worktrees share one namespace: --channel auth is refused while a worktree named auth exists, and both rimz worktree new auth and a launch that would create worktree auth are refused while live agents hold channel #auth. How channels render on screen, with their headers and groups, is the sidebar guide; the launch flags are in cli/agents.md.
Park, steer, interrupt, or schedule
Park for the next turn boundary. This is what a send does with no flags at all. The text holds until the agent finishes the turn it is working on, then lands at the boundary, so it never cuts into work in flight. Use it to hand off follow-up without watching for the agent to free up. The default gate, --on done, waits for a finished turn: one that succeeded, went idle, or ended with the agent waiting on a timer or watch it armed. --on any releases the text after a failed turn too. A turn that stopped on a provider rate limit, spend limit, or overload is paused, not failed, so both gates hold the text until the agent resumes.
rimz message @coder "open a PR summary once tests pass" # waits for the turn to end, then delivers
rimz message --on any @coder "report back either way" # also delivers after a failed turnEither way you get a receipt. It says whether the text went in now or is queued, and when a busy agent caused the queue it names the status and hands you the command that overrides it:
$ rimz message @coder#auth "open a PR summary once tests pass"
queued for @coder#auth (msg_06gc05m0d327d1d0) — @coder#auth is running; send now: rimz message steer msg_06gc05m0d327d1d0
$ rimz message @planner#auth "draft the follow-up plan"
sent to @planner#auth (msg_06gc05nhj9q2b4t7)Nothing is locked in until it lands: rimz message cancel msg_06gc05m0d327d1d0 stops a message still waiting, and rimz message steer msg_06gc05m0d327d1d0 promotes that exact record past its gate.
Other receipts name the opening time, the first unmet --after or --when condition, the blocking message id, or the missing live pane. A resumed provider gets a receipt saying it is starting. When a prompt owns the input, the receipt says your text will not be read until that prompt is answered and points to rimz asks show @coder and rimz answer @coder <choice>. message show gives the same pair of commands for that blocker.
Steer the live turn. --steer writes into the live turn now, the way typing into its pane would, so you can redirect it mid-thought. If RimZ is already writing a command, message, or answer to that pane, steer waits up to 30 seconds for that write to finish, its submit key included, rather than inserting text halfway through it.
rimz message --steer @claude "stop: rebase on main first, the parser moved"Steering an agent that has no live pane yet parks the text instead of dropping it, and delivery follows once the pane appears.
Stop work that is no longer wanted. Typing a correction into a busy pane does not stop its current tool call. Use rimz message --interrupt @claude "stop: the user changed direction" when that work should be canceled: RimZ types your prompt, then presses Escape three seconds later to stop the current turn. This works with Claude Code and Codex; other agents are refused. Codex may already have taken the text into its current turn, or into an open question's answer, so it does not always replay the text as a fresh prompt. RimZ warns if Codex's stop is not confirmed; interrupt receipts explain how to check. An agent that is already resting just receives the prompt, with no Escape. Use --force to send the text and Escape even at a native prompt waiting for input, or --interrupt --wait to print the reply if the text starts a new turn. Keep --steer for a correction that can wait behind the current tool, and plain message for follow-up at a turn boundary.
Schedule a floor under delivery. --schedule holds the text until a wall-clock moment: a duration in s, m, h, or d, or a 24-hour HH:MM time in your configured timezone, with a time already past today meaning tomorrow. The time is an earliest-delivery floor, not an appointment: once it comes due the message parks like any other and lands at the next boundary that takes it. A scheduled message stays out of the queue until it is due, so it never blocks later text to the same agent.
rimz message --schedule 90m @coder "kick off the nightly integration run"
rimz message --schedule 07:30 @planner "draft today's plan from the open issues"What a send does to your machine
Every delivery is the same short sequence against your own multiplexer. RimZ appends the record to ~/.rimz/ws/<workspace-dir>/records/messages/messages.jsonl, resolves the handle to one live pane, and takes that pane's write lock so no other RimZ write interleaves. Then it pastes the text as one bracketed paste, marks the record sent, and presses Enter as a separate key. rimz pane capture @coder shows you the result in the agent's own composer. Nothing reaches the agent by any other route: no API call, no provider-side injection.
A durable record is not a delivery guarantee, and the record is where a failure shows up. A delivery that fails repeatedly before anything reaches the pane ends abandoned. A prompt that reaches the pane but that the agent's reporting hooks never confirm is retyped up to three times and then marked timed_out; a compact command in the same state is never retyped at all, since repeating /compact can discard the context it was meant to save.
Why a parked message is still waiting
A parked message delivers the moment the agent can take it, which means all of these hold:
- Its
--schedulefloor has passed and every--afterand--whencondition is met. - It is the oldest ready message for that agent. Messages to one agent deliver oldest first, and scheduled or condition-held ones step out of line until they are ready.
- The agent is not compacting its context, and its status opens the
--ongate. - No open prompt in its pane is holding the input. A question owns the agent's next input until it is answered, and
--forcesends past one. - A live pane exists to receive the text, and the agent's reporting hooks are installed, since a turn-end hook is how RimZ learns the turn ended. A send that would park for an agent without them is refused up front.
rimz message show names the first unmet condition and the command that clears it, so you never have to guess why a message is still sitting there:
$ rimz message show msg_06gc05m0d327d1d0
msg_06gc05m0d327d1d0 — queued
from: user
to: @coder
channel: auth
created: 2m ago (2026-07-08T15:41:06Z)
TEXT
open a PR summary once tests pass
TIMELINE
EVENT WHEN
queued 2m ago (2026-07-08T15:41:06Z)
DELIVERY CHECK
schedule: ok
fifo: ok
agent: ok
gate: closed (status running, gate done)
ask: ok
pane: ok (zellij:terminal_3)
waiting: @coder#auth is running; gate 'done' opens at next turn end
force now: rimz message steer msg_06gc05m0d327d1d0Hold a message until a condition is met
A message can wait on an agent other than its recipient, which is how you queue a chain of work in one go rather than coming back to start each step. Two flags do it. Both are repeatable, every condition must be met before delivery, and both combine with each other and with --schedule.
rimz message @planner "draft the implementation plan"
rimz message @coder --after @planner "read plan.md and start"
rimz message @planner --when '@coder running 2h' "check @coder: 2h on one turn"--after @handle waits for another agent to finish its queued work. Queue the upstream work before the message that waits on it. An agent that is already idle, with nothing queued and ready for it, satisfies the condition the moment you send. That result then stays satisfied, even if the agent picks up another turn later. The message's own --on gate applies to the agent it waits on, so --on done keeps waiting after a failed turn and --on any releases. Each condition names exactly one existing agent, never a fan-out and never the message's own recipient.
--when '@handle <status> <duration>' waits for an agent to stay in one status. The statuses are running, waiting, idle, success, and failed, and the duration takes s, m, h, or d. A completed turn reads success, not idle. The condition latches the first time it is met, so a busy receiver still gets the text at its next boundary even if the watched agent has moved on since. If the watched agent ends while the condition is unmet, the message is archived with that reason. The watched agent may be the receiver itself, which is what makes a keep-warm message work: rimz message @codex --when '@codex idle 58m' "ping".
An unmet condition steps the message out of the receiver's queue order, so later eligible text still lands. rimz message show names the agent holding the trigger, and rimz message steer forces the record through a missing agent or a dependency you meant to create.
Reach several agents at once
An address that matches more than one agent is an error until you opt into the fan-out, so an ambiguous @claude lists the candidates instead of surprising all of them.
rimz message @all "freeze new work; I'm cutting a release" # every agent in the channel
rimz message --all @claude "rebase on main" # every Claude the address matches
rimz message @codex --worktree feat-a --create "start the auth refactor" # launch one if none existsEach fan-out delivery is prefixed with the selector you typed, so what lands in every pane opens @all, freeze new work; I'm cutting a release and reads as a group message rather than a private one. Deliveries go out one after another, and an agent that cannot take the text right now does not hold up the rest.
When an agent sends @all, RimZ drops that caller and sends to its peers; with no peers it errors rather than talking to itself. An exact handle still permits a deliberate self-message, and an explicit selector fan-out such as --all @claude keeps every match, sender included.
--create launches an agent when the address matches none: a kind (@codex) or a profile (@planner) opens fresh with your text as its first prompt. --worktree feat-a launches it in that worktree, while an inline #feat-a or --channel feat-a launches into that lane.
Compact before the message lands
A long turn can hit the context limit mid-message. An agent compacts by itself only once it hits its ceiling, so a prompt delivered just under that can be cut in half by a compaction firing around it. --smart-compact sends the agent's compact command ahead of your text once its context is full enough, including the configured summary brief where the command accepts one, so the prompt runs against a fresh window.
rimz message --smart-compact 70% @claude "now write the migration guide" # compact first if context ≥ 70% fullGive a percentage of the window (70%) or an occupied-token count (120000, 180k). Omit the flag and RimZ uses [harness] smart_compact, which defaults to 258k; set it to "off" to disable compact-first sends. The flag overrides that machine value (loops → smart compaction, configuration → smart compaction).
Ask and wait for the reply
--wait turns a send into a question: RimZ delivers the text, waits through the reply turn, and prints the agent's final message on stdout. Each target's send receipt and message id appear immediately on stderr, even with --json, leaving stdout for the answer.
rimz message @coder --wait "did the migration land? one line" # the reply alone on stdout
rimz message @all --wait --json "status? one line" # one handle-keyed reply map
rimz message --all @reviewer --wait --any "first verdict?" # return on the first reply
rimz message @codex --wait=5m "open the PR" # bound delivery plus turn
rimz message --steer @claude --wait "answer from this turn" # the live turn is the replyA fan-out wait gathers every reply from the agents' existing contexts and streams them under @handle: lines in completion order, while a failed target writes its forensics to stderr and the others keep gathering. --json buffers one uniform map instead, whether you asked one agent or twenty. The command exits 0 only when every reply turn completed, and a deadline exits 124 with the unfinished targets marked timed_out; the full exit-code table is in cli/message.md.
A deadline does not withdraw queued text: it will still deliver unless you cancel its id with rimz message cancel. The timeout hints distinguish that queued text from text already typed into a pane (do not resend) and a delivered message still being worked on. Use rimz agents logs @coder to read the reply later. The same hints appear on stderr with --json.
Every target must be a running agent with installed and trusted hooks, since hooks are what report the turn's end. --wait conflicts with --create, --schedule, and --no-enter, and --json and --any each require it.
A turn that goes waiting or paused is still inside its reply, so a reply that arms a timer or a watch keeps the wait open until that fires. A bare --wait from your shell waits indefinitely, and from an agent it stops after an hour; pass --wait=<duration> when a script needs its own bound.
Agents wait on each other too, and RimZ catches the deadlock instead of hanging: a send whose target is already waiting on the caller's own reply is refused before anything is queued, and a cycle that forms inside a running fan-out fails that one target, each naming the handle and message that blocks it. Text already queued stays queued and delivers at the next boundary. An agent's own @all --wait gathers peer replies only, never its own.
Send a file or piped input
Inline text is one quoted argument, and it interprets \n as a soft newline in the composer. For anything longer, hand RimZ the content directly.
rimz message @claude --file review-notes.md # the file's text, newlines and all
git diff main | rimz message @reviewer --stdin "review this"--file sends the file as written, with no escape processing and only the trailing newline dropped; an empty file is refused. --stdin reads stdin to EOF, trimming the surrounding whitespace. Alone it is the whole message; combined with inline text, the instruction goes first and the stdin content follows inside <stdin> tags. The two flags are mutually exclusive, and piped stdin without --stdin is ignored with a warning.
Read and steer the queue
Bare rimz message opens the current channel's inbox. Every send is a record you can read back and act on after the fact:
An empty inbox can still have messages elsewhere. Without a target or status filter, RimZ tells you how many messages are in other lanes and how many have not yet delivered, with a command to inspect them. It never mixes their rows into your lane. Use rimz message list '@coder#auth' to select an agent and its lane; a misspelled explicit lane lists the known names instead of showing an empty inbox. (text no longer kept) means a row remains after its text's retention period.
rimz message list # this channel's inbox, newest first
rimz message list --all # every channel, grouped by #channel
rimz message list --system # include waits, signals, subagent digests, and nudges
rimz message show msg_06gc05m0d327d1d0 # full text, timeline, and the first delivery blocker
rimz message edit msg_06gc05m0d327d1d0 --text "…" # revise a queued message before it lands
rimz message steer msg_06gc05m0d327d1d0 # deliver now, past schedule, conditions, and gate
rimz message requeue msg_06gc05m0d327d1d0 # send a finished message again as a fresh record
rimz message cancel msg_06gc05m0d327d1d0 # cancel a queued message; the record stays
rimz message clear @coder # cancel every open message for one agent$ rimz message list
user → @reviewer queued 1m ago msg_06gc05nhj9q2b4t7
read plan.md and start the review · after @coder
user → @coder queued 2m ago msg_06gc05m0d327d1d0
open a PR summary once tests pass
user → @planner delivered 12m ago msg_06gc04t1kkr7c0h9
draft the implementation plan
... 6 system messages hidden (--system shows them)The inbox shows conversation by default: your sends and attributed agent sends. The trailing line counts what the view left out, and --system brings in the traffic RimZ generates for itself, so you can see what woke or nudged an agent. --limit and --status narrow it further.
When something is stuck the loop is always the same: list to find the record, show to name the blocker, then steer, edit, or cancel to clear it.
Ten statuses cover a record's whole life. queued and claimed are still live; sent means the bytes reached the pane and delivered that the prompt's turn started; canceled means you stopped it and archived that the receiver, its channel, or a watched agent ended first; expired means an automatic command waited ten minutes without reaching the pane, so later messages can pass it; timed_out, errored, and abandoned are the three failures. cli/message.md defines each. A finished record keeps its text in the recent history, which is what lets show print it and requeue send it again; requeue refuses a record whose text has aged out of that history. Your prompts and operator compaction commands never expire.
A message can remain sent while RimZ catches up with the agent's hooks. If a hook from that provider, recorded after the send and inside its confirmation window, is still waiting to be processed, RimZ waits rather than sending another copy or timing out a command.
message steer delivers past the schedule, the conditions, the queue order, and the gate. An open prompt still stops it unless you add --force. An expired automatic command cannot be steered; cancel it or requeue it after the sweep records its expiry.
Agents message each other
rimz message is the same command whether you type it or an agent runs it, so a teammate hands work over exactly as you do. The receiving agent sees a three-line header above the text, naming where the prompt came from:
Type: AGENT_MESSAGE
From: @swift-otter (planner)
Content:
read plan.md and startThe first token after From: is the reply address: here, reply with rimz message @swift-otter "got it". The parenthesized label identifies the sender's profile, or its agent kind when it has no profile; a label that repeats the handle is omitted. A team profile such as recon.planner is also omitted when the handle is @planner, including when it has a channel suffix.
An agent should not have to wait forever on a handoff that never arrived. RimZ tells a live agent sender when its message ends undelivered, or stays queued for five minutes with a receiver not working or twenty minutes with a receiver busy in a turn. A queued message stays queued; the notice lets the sender check the receiver or withdraw the message, not resend it blindly. Time spent waiting on the sender's schedule or conditions does not trigger a notice while those conditions remain unmet. Senders using --wait learn failures from the wait itself instead of a separate notice.
Your sends arrive as USER_MESSAGE from @user. RimZ's own deliveries come from @rimz, with the Type naming what produced them:
SUBAGENT_REPORT, the digest sent once an agent's launched subagents have all settled;AGENT_REPORTwhen the fleet includes solo agents.TEAM_REPORT, the leader's final response and the team's board path when an agent-launched team reachesDoneor ends beforeDone.SUBAGENT_PAUSED, sent to a parent once each time one of its subagents stops on a provider usage limit.SUBAGENT_STALLED, sent to a parent once per run when one of its subagents stays silent past the stall window.MESSAGE_UNDELIVERED, sent once to an agent sender when its message ends without delivery.MESSAGE_QUEUED, sent once to an agent sender when its message stays queued past five minutes with a receiver not working, or twenty minutes with a receiver busy in a turn.DEADLINE, the warnings and the stop request a subagent receives as its work deadline nears.WAIT, arimz waitdelivery coming due.CACHE_KEEPALIVE, a facts-only ping that keeps a sleeping agent's prompt cache warm.SIGNAL, a signal-triggered loop firing.STAGE, a team stage opening.
--no-from delivers the text with no header at all, for a script that wants the raw bytes.
An agent's message lands as an ordinary conversation line, so the exchange belongs to the receiver's history like any other. rimz transcript reads a channel back, with each exchange grouped under the message that opened it. Its human view leaves out RimZ's own automation traffic; rimz transcript --json keeps everything. Name a launched child to read its separate conversation: rimz transcript @swift-otter (transcript CLI). When a name could mean several agents, RimZ asks you to choose an address instead of silently opening another conversation. A live agent with no recorded lines reads @swift-otter has no messages yet, followed by its launch age when known and no turn opened if it has not started a turn. An unknown name fails.
To keep watching a conversation, run rimz transcript '#feature' -f. Add -n 10 to start with the last ten entries, then see new entries as they arrive. Stop following with Ctrl-C.
Asks and answers
An agent holding a permission prompt, a plan approval, or a blocking question has handed that prompt its input, and your queued messages wait behind it until it is answered. Answering is part of keeping the fleet moving, so RimZ lets you do it from any shell. Codex can also ask while it keeps working: these async questions appear in rimz asks and as a question line on its running card, without holding your messages. Sending an ordinary or steered prompt clears its listed async questions.
rimz asks # open prompts in this channel; --all for every channel
rimz asks --json # the same prompts as structured data
rimz asks show @planner # one prompt with its context and choices
rimz answer @planner allow # submit a choice by label
rimz answer ask_06gc05m0d327d1d0 2 # or by position, against one exact promptWith no open asks, rimz asks says so in one line and names --all when the list was scoped to your channel. rimz answer drives the agent's own prompt UI with the keys you would press in the pane. One command submits the whole ask: a positional selector per question, commas inside one for a multi-select, and --text for free text. Each async question is a separate ask, answered in the Codex pane: focus it and press Shift+Left. rimz answer refuses these questions without sending input. See async questions for when an answered question leaves the list. An ask with several questions can take mixed choices and text through --json. The ask id is a compare-and-swap token, so a prompt that was already answered or superseded receives no stale keystrokes.
Each row in rimz asks gives you the next command. For permissions, follow rimz agents focus to inspect the full tool call: asks show carries only a labelled summary. Claude, Codex, and Pi accept some answers from the shell, within limits set by what their prompts can confirm. A Claude permission ask offers allow; denial and persistent grants stay in the pane. A Claude plan approval offers the caution-marked approve, which turns on auto-accept edits; keep-planning and refinement text stay in the pane. Success echoes the chosen label and any caution. If confirmation times out, do not resend: use the printed asks show or pane capture command to check. cli/asks.md has the per-agent table. You can always answer in the pane instead, and if you Escape a question and type free text there, RimZ records that text as the open question's answer rather than a new prompt.
See also
- Agents: how handles and profiles come to be.
- Teams: how team roles become handles in a shared channel.
- Worktrees: the isolated tree behind every worktree channel.
- The sidebar: reading the channels and the messages they carry on screen.
- Scripting agents: turning the same handles into one-shot runs with an exit code for a pipeline.
- Loops and schedules: scheduled and handler-driven messages that steer the fleet unattended.
- cli/message.md, cli/asks.md: the exact flags.
- Message internals: the delivery engine underneath.
Worktrees
Agents working in parallel need isolation, and Git already ships the primitive: a worktree (git worktree) is a second checkout of your repository, in its own directory…
Teams
A team launches several agents as one unit, each in a named role with its own context window, model, and instructions, cooperating over messages in a shared channel.