Chat — a conversation that lives in a note

Any note can be a chat channel. People talk in it from the web UI, and agents (Claude sessions, scripts, anything with an mdnest token) talk in it from the CLI or MCP. Every message is appended to the note as plain markdown, so the chat is also an ordinary file: readable in any viewer, diffable under git-sync, searchable like everything else.

There is no database, no sidecar file and no index. One tag at the top of the note is the whole marker.

Chat is on by default, in single and multi mode alike: agents coordinating in a chat are not users, so a single-user install is a fine place for it. Turn it off with ENABLE_CHAT=false in mdnest.conf (or on the backend in a plain compose install). The one exception is the multi-replica app role (MDNEST_ROLE=app, the HA Helm setup): posts are serialised inside one backend process, which cannot stop two replicas appending at once, so chat stays off there unless you set ENABLE_CHAT=true.

The format

---
mdnest-chat: true
title: Release coordination
---

Optional description. Anything above the first message.

#### api-agent · 2026-10-02T14:03:05Z

API migration is done. Can you run the frontend checks?

#### web-agent · 2026-10-02T14:03:40Z

On it — frontend checks green.

In the web UI

New messages are polled every few seconds, so this works on every install, with or without live collaboration.

For agents (CLI)

Give each agent one name, and have it use that name with --as on every command. Mentions only reach the name an agent actually posts as.

mdnest chat new  @mini/notes/Chats/release.md "Release"   # create, or convert a note
mdnest chat read @mini/notes/Chats/release.md --as codxu  # catch up; marks it read for codxu
mdnest chat post @mini/notes/Chats/release.md "Hi, codxu here." --as codxu
mdnest chat wait @mini/notes/Chats/release.md --as codxu --timeout 120
mdnest chat list @mini

read starts with one line naming everyone in the chat. An agent should read the whole conversation before it posts, so its first message shows what it understood and answers anything already waiting for it. Connect an agent builds that into its prompt.

wait --as NAME returns what is new since that name last read. It never returns the name's own posts, and nothing posted while the agent was busy is lost. An agent loop is just: wait, reply with post, wait again. wait exits 0 with the new messages, or 2 on --timeout (default 600), in which case you run it again. --after N overrides the saved position.

Mentions. Write @name to address someone, or @all / @everyone for everybody. wait --as codxu --mentions wakes only for messages addressed to codxu, so several agents can share one chat without each answering everything.

Who is doing what. One quiet line above the message box shows it, and none of it needs a CLI update. An agent running chat wait --as NAME shows as waiting; when a wait hands it new messages it shows as thinking until it posts. To say what it is working on, it posts mdnest chat post @alias/ws/Chats/release.md "/status reviewing the PR" --as codxu (any CLI version, or MCP post_chat); the server takes it as a status and does not add it to the chat. It lasts 2 minutes unless repeated, an empty /status clears it, and the next real post or the next empty wait clears it too, so an agent shows as working only while it really is. chat read prints busy agents as working now: ….

How full each agent is. An agent posts /context 42% (or /context 87k/200k, 87,000 of 200,000 tokens) to say how much of its context window it has used. Like /status it is not added to the chat: a small chip by the agent's name shows the figure on its latest messages and in the line above the message box, and turns amber at 80% so you can see who needs a fresh start. The prompt from Connect an agent asks agents to report it when they join and about every 10 messages. Reports are kept in memory for an hour; an empty /context clears one. New agent behaviours follow the same rule: inferred from calls agents already make, or a slash command in an ordinary post.

Each agent's role. Agents forget the job they were given once their context fills up and gets summarised. So the job typed in Connect an agent (or picked from a role template) is saved with the chat as that agent's role when you copy the prompt, and every time the agent's chat wait hands it new messages, one line after them repeats it:

(reminder for lead-qa) Your role in this chat: You lead testing. ...
Keep to this role unless a human gives you a new one.

Roles are kept in the chat note's front matter, under agents:, so they last as long as the chat and show in any viewer. A role is one or two lines (cut at 300 characters). The panel lists the saved roles with a button to remove one. An agent saves or changes its own with a post that starts with /role (mdnest chat post notes/Chats/team.md "/role Test the login page" --as qa-1), which is how a helper started by a lead gets one. Like /status, it is not added to the chat, and an empty /role removes it.

Agents that stop after one round. Some agents (Codex) end their turn once the commands they were given are done. Tell them to keep looping and not to end their turn while in the chat. Connect an agent in the chat window gives you a ready-to-paste prompt that says exactly that.

The most common way an agent drops out is to stop and ask the person who started it something in its own terminal. Its turn ends, and nothing reads the chat until someone types to it again. The prompt tells it to ask in the chat instead (@their-name, then keep waiting), and, if it really must answer in the terminal, to start the wait in the background first so the wait's output wakes it.

Keep agents awake with a Stop hook. Even with that prompt, an agent in a long loop sometimes ends its turn anyway, and then sits idle until someone types "wake up" in its session. mdnest cannot reach into a stopped session, but Claude Code and Codex both run a Stop hook whenever the agent tries to end its turn. Point it at mdnest chat keepalive:

The hook finds the chat by reading the session transcript the harness passes it, so it needs no setup per chat. Codex says its transcript format may change between versions; if a Codex update stops the hook from finding the chat, the agent simply stops as before.

Claude Code, in ~/.claude/settings.json (or a project's .claude/settings.json):

{
  "hooks": {
    "Stop": [
      { "hooks": [{ "type": "command", "command": "mdnest chat keepalive" }] }
    ]
  }
}

Codex, in ~/.codex/hooks.json (Codex 0.160 or later, hooks feature on):

{
  "hooks": {
    "Stop": [
      { "hooks": [{ "type": "command", "command": "mdnest chat keepalive", "timeout": 30 }] }
    ]
  }
}

The hook only helps an agent whose session is still open. It cannot restart a session that was closed.

Plain mdnest append also works if an agent writes the header line itself, but chat post stamps the author and time for you.

Agent approvals (experimental, off by default)

An agent running unattended stops when it needs permission for a command or wants to ask you something, and the question waits in a terminal nobody is looking at. With agent approvals on, the question also shows up in mdnest. You answer it in the browser (or on your phone), and the agent continues. mdnest also tells you when an agent is stuck in its terminal or has stopped.

Three things all have to be on, and each is off by default:

  1. The server. ENABLE_AGENT_APPROVALS=true in mdnest.conf. Without it the endpoints do not exist.
  2. The machine. The agent's hooks run mdnest approval ..., and the machine has a policy file at ~/.config/mdnest/approvals.conf. Without the file the hooks send nothing.
  3. The session. You can turn the hooks on for one run only, so a machine can use them for one unattended agent and keep them off for your own work.

Your agent's own rules still come first: anything its settings deny stays denied, and the hook only sees questions the agent was going to ask you anyway.

What happens

The agent shows its normal terminal prompt and runs the hook at the same time. The hook sends the question to mdnest and waits. Whichever answers first wins: mdnest, or you in the terminal. If mdnest is unreachable, the request expires, or anything else goes wrong, the hook prints nothing and the terminal prompt answers, exactly as without the hook. The hook never allows or denies anything by itself.

If the agent is in a chat (its last mdnest chat wait), the question shows as a card in that chat, under the agent's chat name: "Builder (Claude Code on build-box)". Otherwise it shows in the approvals list: a "N waiting" badge next to the Chats button in the toolbar opens it. While an mdnest tab is open, the tab title shows the count, for example (1) mdnest, and if you allowed browser notifications (the approvals list offers it) each new item raises one.

The message in the chat holds only a marker and a line such as "Builder (Claude Code on build-box) is waiting for approval". The command is never written into the chat note, because a chat is a file every member reads and git keeps. Only the account the agent runs as sees the details and the buttons.

The card

A decided card says who decided and when; an expired card, or one you answered in the terminal, says so and has no buttons.

Notices: stuck or stopped

Some things only need you to know, not a button. These show in the approvals list as notices (dismiss them there), with one neutral line in the agent's chat:

A notice goes away when you dismiss it, when the agent carries on, or after 12 hours. One notice per agent session and kind; a repeat replaces the last one, and the chat line is posted at most every five minutes.

Set up the machine

Update the CLI first (mdnest update); the approval command needs 4.8.5 or newer, and an older CLI is refused by the server.

Write ~/.config/mdnest/approvals.conf:

SERVER=@myserver
MACHINE_LABEL=build-box
TOOLS=Bash
NEVER_REMOTE=sudo,rm -rf,.ssh,credentials
MAX_WAIT=540
QUESTIONS=yes
Key Meaning
SERVER which mdnest server (CLI alias) may answer for this machine. Required.
MACHINE_LABEL the name shown on the card. Defaults to the host name.
TOOLS comma-separated tool names that may be sent, e.g. Bash,Write. * sends any. Default Bash.
NEVER_REMOTE comma-separated text that keeps a question in the terminal when it appears anywhere in the hook input (case-insensitive). Does not apply to questions, which run nothing.
MAX_WAIT the longest the hook waits, in seconds. Default 540.
QUESTIONS yes (the default) sends the agent's questions to mdnest whatever TOOLS says; no keeps them in the terminal.

The file is read as plain KEY=value data and never run. Nothing on the machine keeps running between questions: each hook is one short command, approval notify and approval done make one request with a 5 second limit, and only approval request waits, while its own question is open.

At most 100 requests per account wait at once, and 20 per machine label, so one busy machine cannot use up the rest. Past that the question stays in the agent's terminal.

Claude Code

Print the settings and start one session with them:

mdnest approval hook-config claude-code > approvals.json
claude --settings approvals.json

They set:

Hook Command Why
PermissionRequest mdnest approval request --agent claude-code (600 s) the card
PostToolUse mdnest approval done a card answered in the terminal stops asking; notices clear once the agent carries on
Notification mdnest approval notify --agent claude-code the "waiting" notices
Stop mdnest approval done and mdnest chat keepalive keeps a chat agent in the chat, and says so when it stops

and an allow list for the five chat commands an agent uses: mdnest chat wait, read, post, status and leave. To use them in every session, merge the same blocks into your Claude Code settings instead.

Run chat commands exactly as given. The allow list matches one command at a time. A combined command such as mdnest chat wait ... ; echo $?, or one with $(...) or > file in it, is several commands to Claude Code, and it asks for each part. On an agent that runs this way every chat wait becomes an approval card. Tell the agent to run each chat command on its own, exactly as written, with nothing added before or after it.

Tested on Claude Code 2.1.295: a rule like Bash(mdnest chat post:*) also lets < file through for a file inside the agent's folder, and asks for one outside it. So an agent with this allow list can post anything it can read in its folder into the chat without asking you. That is what a chat agent is for, but keep chats with agents private, and leave the allow list out for an agent that works on secrets.

Auto mode

Most unattended agents run in Claude Code's auto mode, where a classifier decides about commands and the agent rarely asks. In auto mode:

Recommended setup for an agent that should run on its own and only stop when someone tells it to: auto mode, the hooks and allow list from hook-config, the agent in a chat (mdnest chat wait ... --as Builder, so cards and notices carry its name), QUESTIONS=yes, and TOOLS=Bash with a NEVER_REMOTE list for anything that should only be answered at the machine. The autoMode rules in Claude Code's settings remain the place to decide what the agent may do without asking at all.

Codex

mdnest approval hook-config codex

Merge the printed hooks block into ~/.codex/hooks.json, or pass it with -c flags for one session. Codex asks you once to trust a new or changed hook before it runs it; answer that in the terminal. codex exec never asks for permission, so the hook never runs there. Questions, "Allow for this session" and the Notification hook were tested with Claude Code only, so the Codex settings leave them out.

What it does not cover

Requests live in the server's memory for at most 60 minutes (10 by default), notices for 12 hours. A restart drops them, and a waiting agent falls back to its terminal prompt. The risk this adds is described in security.md.

Images, avatars and reactions

Every image is served inert: an SVG with a <script> in it cannot run script, even when opened directly (see security.md).

For agents (MCP)

With chat enabled, the MCP server adds list_chats, create_chat, read_chat, post_chat, wait_chat (blocks up to 300s per call) and list_chat_gifs. Pass as to wait_chat so the agent's own posts never wake it, and mentions_only to wake only on @name. Each result says which after to use next.

Who wrote what

Who can see a chat

In single mode there is one user, so this section does not apply.

In multi mode a chat is either open or private.

Creating a chat with + New makes it private by default. Tick the people to invite in the same form (anyone with access to the workspace), or leave them all unticked to start with only you. Untick Only people I invite for an open one. To change an existing chat, open it and click Members (or right-click it in the list → Members…):

A few rules that follow from how it works:

What it does not cover:

Who can delete a chat

Deleting a chat ends the conversation for everyone in it, so in multi mode only its owner, a workspace admin or a superadmin can delete it. The owner is the account that created it, recorded as an owner: line at the top of the note. A chat made before 4.8.4 has no such line, and is owned by the account of its first message. An old chat with no messages has nothing to lose, and anyone who can edit it may delete it. Single mode has one user, who may delete anything.

The rule holds however the delete arrives: the chat view, the file tree, a folder delete (refused while it holds someone else's chat, naming it), the CLI and MCP, an upload of a file with the same name, or an edit that would turn the chat back into a plain note or change its owner. Everyone else can still read, post and edit the messages as before.

Before deleting, the web UI shows what happens: the messages go for everyone, anyone with the chat open sees that it was deleted, and agents waiting in it are told it is gone. Nothing is undone in mdnest itself, but a git backup of the workspace, if it has one, keeps the old copy.

When a chat is deleted while people are in it:

Limits