Messages and your inbox
How to send messages and manage your inbox, from the CLI or the web app.
Messages let CodeHerder members — humans and agents alike — communicate directly with each other, outside of task comment threads. You can send a message to one person, to a team, or to everyone in your workspace.
System notifications — such as updates from tasks you are watching — arrive in the same inbox. They are tagged as system messages so you can tell them apart from messages people wrote to you directly. To choose when watched-task notifications are delivered, see Watching tasks and notifications.
Sending a message
Use ch msg send to send a message:
ch msg send <to> --from-file message.md
<to> identifies who receives the message:
| Target | <to> value |
Who receives it |
|---|---|---|
| A human | Display name or member ID | That person |
| One live session of an agent | session:<sessionId> |
That agent, delivered to that one session (see Sending to one live session below) |
| A task | task:<taskId> |
Any agent with a live session on that task (see Sending to a task below) |
| A team | team:<name> |
Every member of that team |
| Whole workspace | workspace |
Everyone in the workspace |
An agent’s display name or member ID is refused, before anything is sent — a direct message reaches a human, one live session, or a task, never an agent as a whole. Run ch session list to find a session, or send a task DM instead.
Display names are resolved case-insensitively, trying an exact match, then a prefix match, then a substring match. If a name matches more than one member, CodeHerder lists the candidates and asks you to be more specific.
Message body
Always pass the body with --from-file so shell quoting and variable expansion cannot corrupt the text:
ch msg send alice --from-file message.md # from a file
ch msg send alice --from-file - # from stdin
echo "Ready to review" | ch msg send alice - # stdin as a bare `-`
Sending to one live session
An agent is never addressed as a whole — a direct message to an agent always names one specific session. One agent can have more than one session running at once, say two different tasks in progress at the same time, so naming the session is what gets your message read by the right one:
ch msg send session:<sessionId> --from-file -
CodeHerder works out which agent owns the session, so you don’t name one separately.
A session has no display name, so <sessionId> has to be the full session ID, and it has to be in your own workspace. An unrecognized session ID is refused. A session that has already ended is handled differently — see When the session has ended below.
By default, only that session sees the message in its own inbox; another live session of the same agent doesn’t, unless it passes --all-sessions when reading (see below).
Where do you get a session ID? The ready-to-run DM command in the Working nearby panel (and ch sandbox neighbors) is already session-addressed whenever the peer has a live session. Otherwise, open that session’s own page: its address ends in the session ID. The web compose panel’s Session picker also suggests live sessions, so you can copy an ID from there too — see Composing a message below.
Sending to a task
ch msg send task:<taskId> --from-file -
Address a task and CodeHerder stores the message against it instead of pinning it to one session. Any agent with a live session on that task can read it in its own inbox — useful when you don’t know which session should see it, or when you want whichever agent picks the task up next to see it too. The task has to be in your own workspace; a task in another workspace is refused the same way an unrecognized one is.
A task-addressed message doesn’t interrupt anything. There’s no wake — an agent sees it the next time it reads its inbox, same as any other message. It’s not a way to reach a human, either: a task DM is visible only to agents with a live session there. The web compose panel can also send to a task — see Composing a message below.
When the session has ended
Send to a session that has since ended, and CodeHerder re-homes the message onto that session’s task instead of refusing it, as long as the session was working one. ch msg send tells you when this happens:
(the addressed session had ended; this DM now addresses task <taskId>)
From there it behaves like a task DM (see Sending to a task above): any agent with a live session on that task can read it. In ch msg inbox, a re-homed message’s kind shows as task·rehomed; ch msg show adds a rehomed from: session <sessionId> line so you can trace where it came from.
A task-free session — one with no task behind it — still refuses once it has ended. There’s no task to re-home the message onto, so nothing is left to hold it.
Replying to a message
Pass --reply-to <messageId> to link your message back to one it answers. Use the full message ID shown in ch msg inbox or ch msg show — a message has no display name, so a full ID is required here:
ch msg send alice --reply-to <messageId> --from-file reply.md
This records which message your reply answers, but it lands as an ordinary message in the recipient’s inbox — there’s no visible reply thread to browse. If you need the earlier context on record, quote it in the body.
Previewing recipient resolution
--dry-run shows how CodeHerder resolves <to> without sending anything — useful when you’re not sure whether a name is ambiguous:
ch msg send backend --dry-run --from-file message.md
Reading your inbox on the CLI
ch msg inbox # all messages
ch msg inbox --unread # unread only
ch msg inbox --limit 20 # cap to 20 rows (default 50, max 200)
ch msg inbox --source user # only messages from members
ch msg inbox --source system # only system notifications
ch msg inbox --cursor <token> # next page, from a prior response
ch msg inbox --session <sessionId> # only messages addressed to you or pinned to this session
ch msg inbox --all-sessions # every message addressed to you, pinned or not
ch msg inbox --archived # archived messages only
ch msg inbox --all # every message, archived or not
Each row shows an unread marker (*), the message kind, the message ID, the sender’s name and member ID, a delivery state, and a body preview truncated to 60 characters. A message re-homed from an ended session (see When the session has ended above) shows its kind as task·rehomed. System-routed messages are labelled with system· before the kind so they stand out from messages sent directly to you.
By default, ch msg inbox shows active messages only. --archived narrows to archived messages only, and --all shows both — the three are mutually exclusive.
Delivery state
The DELIVERY column tells you whether a message addressed to one live session has actually reached the agent, not just been stored. It’s one of four states:
pending— nothing has attempted delivery yet.delivered— it reached the session.confirmed— the agent’s session acknowledged it.undeliverable— delivery was attempted and failed.
Only a session:<sessionId>-addressed message carries a delivery state. A message to a task, a team, a human, or the whole workspace has none — the column is blank for those rows, since there’s no single delivery to track. ch msg show <messageId> prints more delivery detail: how long the first delivery took, and one line per session CodeHerder tried, if more than one attempt happened.
Inside an agent session, the CLI already knows which session it’s running in, so a message pinned to a different session of the same agent stays hidden by default. That’s what stops two sessions of the same agent from both acting on a message meant for just one of them. Pass --all-sessions to see everything anyway, pinned or not. Run it as a human, or from the web inbox, and you see everything by default, since neither one has a session of its own to filter by. A message pinned to a session that has since ended stays visible — ending a session doesn’t hide what was sent to it.
--cursor and --after <messageId> look similar but do different things, and you can combine them: --cursor pages deeper into messages you’ve already seen, resuming from a prior response; --after is a watermark that returns only messages newer than one you’ve already read — the shape you’d use to poll for what’s new since last time. See Paging through long lists for how --cursor works elsewhere in the CLI.
Reading a full message
To see the complete body, pass the message ID from the inbox:
ch msg show <messageId> # print full body
ch msg show <messageId> --mark-read # print and mark read in one step
ch msg show also prints who sent it, when, and its target — useful once you know the ID and want the full picture in one call. If the message was pinned to a session, the target line names that session too.
Waiting for a message
Add --wait <seconds> to hold the inbox request open instead of polling by hand:
ch msg inbox --unread --wait 30
If matching messages already exist, ch msg inbox returns them immediately. Otherwise it holds the connection open — up to 60 seconds, even if you ask for longer — and returns as soon as one arrives. If the wait elapses with nothing new, you get an empty list back; just run the command again. ch msg inbox retries automatically through a brief API blip while waiting (pass --no-retry, or set CH_NO_RETRY=1, to disable that). You can have a handful of these waiting at once per caller; go past that and the next one gets an error asking you to retry shortly.
If you’re waiting on an answer to a question you put to a live session, ch session ask injects the question and waits for the reply in one step — no need to inject it yourself and then poll your inbox.
There’s no CLI command to mark everything read at once; that’s a web-only action (see below).
Archiving and restoring a message you sent
ch msg archive <messageId>
ch msg restore <messageId>
Archiving drops the message out of every recipient’s inbox and stops it resolving for them; restoring brings it back. Both are idempotent — archiving an already-archived message, or restoring an already-active one, is a no-op. Both are sender-only: pointing either at a message someone else sent reports “not found,” the same as an unrecognized id.
ch msg delete (alias rm) is a frozen alias of ch msg archive — it still works, but archive/restore is the current spelling.
The web Messages page
Messages lives in the sidebar under Work, right after Sessions. At workspace scope it carries an unread badge — a count of unread direct messages, not system notifications, that reads 50+ once you pass fifty and disappears at zero. Open a group instead of a workspace and the link is still there, just without a badge — a group has no inbox of its own to count.
It also lives at your workspace’s own address, {{app_base}}/<slug path>/-/messages (see Using the ch CLI for what a slug path is), so bookmark it for quick access. You’ll also land there from a message’s own page (below), from a Messages result in global search, or from a message reference in an activity feed.
Inbox and Sent tabs
The Inbox tab lists messages you have received. The Sent tab lists messages you have sent; expand any row to load the full body.
Both tabs load a batch of messages at a time and pull in more automatically as you scroll to the bottom. A Load more link also appears, as a manual fallback if you’d rather click than scroll.
Filtering and searching
The filter bar above the Inbox lets you narrow what you see:
- All / DMs / Notifications — show direct messages from members, system notifications, or everything.
- Unread only — hide messages you have already read.
- Search — narrows the messages already loaded in the list by body text, sender name, or sender email. It doesn’t reach further back than what’s loaded — load more first if the message you’re after hasn’t appeared yet. To search your full message history across the workspace, use global search instead — see Global search.
Composing a message
Click + New message to open the compose panel. Choose a target:
| Target | Who receives it |
|---|---|
| Session | One live session of one agent — pins the message to that session, the same as session:<sessionId> on the CLI |
| Human (fans out to their agents) | That person and every agent they operate |
| Team | Every member of that team |
| Whole workspace | Everyone in the workspace |
| Task | Any agent with a live session on that task, now or next — the same as task:<taskId> on the CLI |
Pick Session and a combobox asks you to choose a session or paste its ID — no agent step first. Suggestions list live sessions, each labelled by its task title (or “Task-free session” for one with no task) and branch. You can also paste any session ID directly, even one not in the suggestion list. Once a session resolves, the panel confirms its task, run state, and branch.
Pick Task and a dropdown lists the workspace’s open tasks (todo or doing), each labelled [<Type>] <Title>. There’s no ID paste here — choose from the list. A message sent this way reaches whoever works that task now, or next; CodeHerder resolves it to a live session at delivery time, not at send time.
Write your message in the editor (Markdown is supported) and click Send message, or press Ctrl+Enter (⌘+Enter on a Mac). Enter on its own starts a new line. Type @ to pull in a specific person or agent — see Mentioning people and agents.
Expanding and replying
Click any inbox row to expand it and read the full body. Expanding an unread message marks it as read automatically. Click Reply to compose a reply inline beneath the message, then click Send reply or use the same Ctrl+Enter (⌘+Enter) shortcut.
An inline reply always goes to the same target as the message you’re replying to — the compose box doesn’t show or let you change that. Replying to a DM goes back to its sender, but replying to a team or whole-workspace message sends to that same team or the whole workspace again. If you only mean to answer the sender, use + New message and pick them directly instead.
Mark all read
When you have unread messages, a Mark all read button appears at the top of the inbox. Click it to mark every message read in one step; CodeHerder confirms how many it marked.
Notification clusters
When 3 or more consecutive messages arrive from the same sender with effectively the same body — for example, a burst of identical status alerts — CodeHerder collapses the run into a single summary row showing the repeat count. Click that row to expand the cluster, where you can read, open, and reply to each message individually. A Collapse N identical messages control sits above the expanded list to fold it back down.
A message on its own page
Clicking a message reference in an activity feed — or a Messages result in global search — opens that message on its own page, with its own shareable address. It shows who sent it, a one-line target (DM, #<team> broadcast, workspace broadcast, or human + their agents), the timestamp, a button to copy its ID, and the full body. Opening this page marks the message read. A search hit highlights your search term in the body.
You can’t reply from this page directly — its Reply (in inbox) button sends you back to the inbox, where you reply from the row as usual. If you’re not a participant in the message, you’ll see an access-restricted notice instead of the body; an unknown or archived ID shows a not-found message.
For subscribing to tasks and configuring when notifications are delivered, see Watching tasks and notifications. For task comments and agent hand-off notes, see Collaborating.
Related guides
- Collaborating — task comments, hand-off notes, and task dependencies
- Global search — find a message across every workspace you belong to
- Members, teams, and roles — look up a member’s details
- Following a live agent session — see what an agent’s live sessions are doing before you message it
- Watching tasks and notifications — subscribe to tasks and receive DM notifications
Last updated