# Hermes

[Hermes Agent](https://hermes-agent.nousresearch.com/) is an open-source AI agent from Nous Research that runs on your own machine and connects to messaging platforms like Telegram, Discord, Slack, LINE, and now Roam. With Roam, you can run your agent **directly inside Roam chat** — users DM it, mention it in groups, and pull it into threads, the same way they would any teammate.

This guide is about exactly that: running a Hermes bot in Roam chat. There are two flavors of bot (Personal and Org); pick whichever fits your use case and follow the matching section. You can run both side by side from the same machine.

## Prerequisites

- A working [Hermes Agent install](https://hermes-agent.nousresearch.com/docs/installation). Hermes is a CLI that runs on macOS or Linux (Windows via WSL); confirm it works with `hermes --version`.
- A Roam workspace where you can create either a Personal Access Token (for a Personal Bot) or an API Key (for an Org Bot).
- A way to expose a local HTTPS endpoint so Roam can deliver webhooks. [ngrok](https://ngrok.com/) is easiest for dev; [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) is the recommended free option for long-running setups.

## Pick your bot

|                       | **Personal Bot**                                                                                              | **Org Bot**                                                                                                          |
| --------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| **Acts as**           | You (the user who created the token).                                                                         | Its own persona (e.g. "OnCallBot"), owned by the workspace.                                                          |
| **Who can talk to it**| Only people you allowlist. Other users are dropped.                                                           | Anyone in the workspace can DM it, @-mention it, or pull it into a thread, subject to the allowlists you configure.  |
| **In groups**         | Cannot be added as a member (Roam restricts PAT bots to user-typed accounts). Pulled into chats by @-mentions in your own conversations. | Can be added as a member of any group (required for **private** groups). In **public** groups, @-mentions work without membership. |
| **Token type**        | Personal Access Token (`rmp-…`).                                                                              | API Key (`rmk-…`).                                                                                                   |
| **Best for**          | Personal workflows — research, summarizing your own meetings, drafting messages on your own data.             | Team workflows — on-call assistant, FAQ bot, status reports, anything everyone needs to talk to.                     |

Run both at once if you need both — see [Run additional bots](#run-additional-bots).

## How access control works (read this first)

Hermes' Roam adapter has three layers of access control, in order:

1. **Token type filtering.** A Personal Access Token discovers its owner from `/v1/token.info` at startup. The owner's name appears in the agent's context (so the agent addresses you by name), but the plugin does **not** restrict messages to that owner automatically — you control who can talk to a PAT bot via the allowlist below.
2. **Sender allowlists.** Set `ROAM_ALLOWED_USERS` (DM senders) and `ROAM_ALLOWED_GROUPS` (group/chat IDs) to comma-separated UUID lists, or set `ROAM_ALLOW_ALL_USERS=true` to skip allowlisting entirely. For development, `ROAM_ALLOW_ALL_USERS=true` is the easiest start; for production, an explicit allowlist is the safer default.
3. **Mention gate** (groups only). `ROAM_REQUIRE_MENTION=true` makes the bot silent in groups unless someone @-mentions it. Defaults to `false` (responds to every message).

The boundary you actually rely on for safety is the allowlist. For a Personal Bot in dev, put just your own user ID in `ROAM_ALLOWED_USERS`; for an Org Bot, set `ROAM_ALLOWED_GROUPS` to the chat IDs you want it to live in. Workspace membership in Roam is also a coarse boundary — an Org Bot can only see messages from groups it's been invited to.

## Set up a Personal Bot

A Personal Bot acts as you. DM it for one-on-one help, or @-mention it from groups you're in to pull it into a conversation on your own behalf. It can't be added to groups as a member.

### 1. Create a Personal Access Token in Roam

**Prerequisite:** Your workspace admin must enable Personal Access Tokens (see [Admin Policy for Personal Access](/docs/guides/access-models#admin-policy-for-personal-access)).

1. In Roam, open **User Settings > Developer**.
2. Click **Create Personal Access Token**.
3. Name it (e.g. "Hermes").
4. Select the permissions to grant — at minimum **Chat Send** and **Chat Read** so the agent can send and receive messages. Add **User Read Email** if you want the agent to know your email, plus **Meetings Read** and other scopes for the features you need.
5. Upload an avatar image — this is the picture shown next to the agent's messages in Roam chat.
6. Click **Create Token**. Copy both the **token** (starts with `rmp-…`) and the **webhook signing secret** (`whsec_…`) shown on the confirmation screen — you'll need both in step 3.

### 2. Approve the token

If your workspace requires admin approval for personal access (the default), an admin must approve the new token before it can be used:

1. In Roam, open **Roam Administration > Developer > Personal Access Tokens**.
2. Find the new token and click **Approve**.

If your workspace is set to **Auto-approve**, skip this step. See [Admin Policy for Personal Access](/docs/guides/access-models#admin-policy-for-personal-access).

### 3. Install the Hermes Roam plugin

Hermes discovers user plugins under `~/.hermes/plugins/`. Drop the plugin tarball there:

```bash
VERSION=v0.0.7  # check https://github.com/WonderInventions/hermes-roam/releases for newer
mkdir -p ~/.hermes/plugins && \
  curl -sL "https://github.com/WonderInventions/hermes-roam/releases/download/$VERSION/hermes-roam-plugin-$VERSION.tar.gz" \
  | tar -xz -C ~/.hermes/plugins && \
  hermes plugins enable roam
```

The plugin's [INSTALL.md](https://github.com/WonderInventions/hermes-roam/blob/master/INSTALL.md) always tracks the latest tag.

Confirm it loaded:

```bash
hermes plugins list | grep roam
```

You should see `roam | enabled | <version> | Roam ... | user`.

### 4. Configure the plugin

Add the credentials to your default profile's environment file at `~/.hermes/.env`:

```bash
cat >> ~/.hermes/.env <<EOF

# Roam (ro.am) platform plugin
ROAM_API_KEY=<paste your rmp-… token>
ROAM_WEBHOOK_SECRET=<paste your whsec_… secret>

# For dev: allow any sender. For prod, switch to ROAM_ALLOWED_USERS=<your UUID>.
ROAM_ALLOW_ALL_USERS=true
EOF
```

### 5. Expose a public webhook URL

Roam delivers webhooks to a public HTTPS URL. The plugin's webhook server listens on `localhost:8647/roam/webhook` by default. Start a tunnel pointing at port 8647:

```bash
ngrok http 8647
```

ngrok prints something like `https://<random>.ngrok.app`. Add it to `~/.hermes/.env`:

```bash
echo "ROAM_WEBHOOK_PUBLIC_URL=https://<random>.ngrok.app" >> ~/.hermes/.env
```

With `ROAM_WEBHOOK_PUBLIC_URL` set, the plugin auto-calls `webhook.subscribe` on startup so you don't need to register the URL manually in Roam admin.

### 6. Start the gateway

```bash
hermes gateway restart
tail -f ~/.hermes/logs/gateway.log | grep -i roam
```

You should see:

```
roam: PAT owner=<Your Name> (id=…); bot persona=<Bot Name> (id=…)
roam: subscribed to chat.message webhooks at https://….ngrok.app/roam/webhook
roam: webhook listening on 0.0.0.0:8647/roam/webhook (public: https://….ngrok.app)
✓ roam connected
```

The `PAT owner` line confirms the plugin read your identity from `/v1/token.info` — the agent will now address you by name in conversation.

### 7. Verify

In Roam, DM your bot. It should reply. The first message in any new chat triggers Hermes' onboarding flow ("Hi, I'm Hermes…"); send a second message to see a real LLM reply. To pull the bot into a group you're already in, @-mention it — its webhook fires for that single message, and it replies in-thread under your mention.

## Set up an Org Bot

An Org Bot is its own persona — anyone in the workspace can DM it for help, @-mention it from any chat to pull it into a thread, or add it as a member of a channel where it responds to every message (your choice).

Hermes' multi-bot pattern is **profiles**: one Hermes profile per bot, each with its own gateway, state, and credentials. We'll create a new profile called `org` for the Org Bot.

### 1. Create an API Key in Roam

You'll need workspace-admin access for this step.

1. As a Roam admin, open **Roam Administration > Developer**.
2. Create a new API Client and select **API Key** as the authorization type.
3. Give it a name (e.g. "OnCallBot") and select the scopes for what the bot will do — at minimum:
   - `chat:send_message` — Send messages
   - `chat:read` — List groups it's added to
   - `chat:history` — Read recent messages in those groups (used for context)
4. Add additional scopes as needed: `meetings:read`, `recordings:read`, `transcript:read`, `groups:read`, etc.
5. Upload an avatar image and set a display name — this is what users see in Roam chat.
6. Click **Create**. Copy both the **token** (`rmk-…`) and the **webhook signing secret** (`whsec_…`).

You'll add the bot to specific groups in [step 7](#7-invite-the-bot-to-your-channels), once the Hermes side is wired up.

### 2. Create a Hermes profile

Each profile is an isolated Hermes instance — separate gateway, separate state, separate plugin install. See Hermes' [Profiles guide](https://hermes-agent.nousresearch.com/docs/guides/profiles) for the full model.

```bash
hermes profile create org --description "Org bot for #engineering, runs OnCallBot persona"
```

This creates `~/.hermes/profiles/org/` and an `org` shim in `~/.local/bin/` so you can run `org <subcommand>` instead of `hermes -p org <subcommand>`.

### 3. Install the plugin in the org profile

Plugins are per-profile, so re-install into the new profile's plugin directory (use the same `$VERSION` you installed for the Personal Bot):

```bash
mkdir -p ~/.hermes/profiles/org/plugins && \
  curl -sL "https://github.com/WonderInventions/hermes-roam/releases/download/$VERSION/hermes-roam-plugin-$VERSION.tar.gz" \
  | tar -xz -C ~/.hermes/profiles/org/plugins
```

Enable it for this profile (writes to the org profile's `config.yaml`):

```bash
mkdir -p ~/.hermes/profiles/org && cat > ~/.hermes/profiles/org/config.yaml <<EOF
plugins:
  enabled:
    - roam
EOF
```

If you want the org profile to share the default profile's LLM model setup (recommended for parity), append the model block to that config too:

```bash
grep -A 4 "^model:" ~/.hermes/config.yaml >> ~/.hermes/profiles/org/config.yaml
```

You may also need to copy auth tokens for OAuth-based providers (e.g. xAI Grok via OAuth):

```bash
cp ~/.hermes/auth.json ~/.hermes/profiles/org/auth.json
```

### 4. Configure credentials and behavior

The org profile reads its environment from `~/.hermes/profiles/org/.env`:

```bash
cat > ~/.hermes/profiles/org/.env <<'EOF'
# Roam (ro.am) platform plugin
ROAM_API_KEY=<paste your rmk-… token>
ROAM_WEBHOOK_SECRET=<paste your whsec_… secret>

# Each profile binds its own webhook port. The default profile uses 8647;
# use 8648 (or any free port) for the org profile.
ROAM_WEBHOOK_PORT=8648

# Org bots that idle in busy channels usually want every reply in a thread
# off the post being answered, so the main channel stays clean.
ROAM_REPLY_IN_THREAD=true

# Mention gate. false = respond to every message in groups. true = silent
# unless @-mentioned. Choose based on whether this bot lives in dedicated
# channels (false) or general ones (true).
ROAM_REQUIRE_MENTION=false

# For dev: allow any sender. For prod, switch to:
#   ROAM_ALLOWED_GROUPS=<comma-separated chat UUIDs>
ROAM_ALLOW_ALL_USERS=true
EOF
```

If you set up the LLM model in [step 3](#3-install-the-plugin-in-the-org-profile) but want any non-OAuth API keys too (OpenAI, Anthropic, OpenRouter, etc.), copy them from the default profile:

```bash
grep -E "^(OPENROUTER|XAI|ANTHROPIC|OPENAI)_" ~/.hermes/.env >> ~/.hermes/profiles/org/.env
```

### 5. Expose a public webhook URL for the org bot

The default profile's tunnel forwards to port 8647; the org profile needs its own forwarding to port 8648. Start a second tunnel:

```bash
ngrok http 8648
```

Take note of the new `https://<random>.ngrok.app` URL and add it to the org profile's `.env`:

```bash
echo "ROAM_WEBHOOK_PUBLIC_URL=https://<random>.ngrok.app" >> ~/.hermes/profiles/org/.env
```

For a long-running setup, [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) is the recommended alternative to ngrok.

### 6. Give the bot a persona

A new bot has no behavior of its own — it'll respond like a generic assistant. Edit `~/.hermes/profiles/org/SOUL.md` to set the bot's personality, role, and prompt:

```text
You are OnCallBot, an SRE assistant living in #alerts in Roam. Every
message in this channel is typically a Datadog alert. Your job is to
investigate and report.

When a new alert arrives:
1. Extract the monitor name, the failing condition, the affected service /
   host / tag(s), and the alert time window.
2. Investigate using the `datadog` tool: monitor details, metrics for the
   affected service around the alert time, related logs, recent deploys.
3. Reply with a concise, well-structured investigation:
   - **Summary** — one sentence: what's wrong and the likely cause.
   - **Evidence** — specific metric values, log excerpts, exact times.
   - **Suggested next steps** — what a human on-call would do next.
4. Keep replies tight. No filler.
5. If you can't form a hypothesis, say so plainly — list what you looked at.
6. Never claim a fix is applied. You investigate and report; humans act.

When asked who you are, say you are OnCallBot.
```

See Hermes' [Configuring the agent](https://hermes-agent.nousresearch.com/docs/guides/configuring-the-agent) guide for the full personality model.

### 7. Invite the bot to your channels

In Roam, open **Group Settings** for the channel you want the bot to live in and add it as a member. This is required for private groups (the bot only sees messages in groups it's been added to). In public groups, @-mentions reach the bot without explicit membership, but adding it as a member is still the cleanest way to scope its presence.

Repeat for each channel the bot should hang out in.

### 8. Start the gateway and verify

```bash
org gateway start
tail -f ~/.hermes/profiles/org/logs/gateway.log | grep -i roam
```

You should see:

```
roam: org token; bot=OnCallBot (id=…)
roam: subscribed to chat.message webhooks at https://….ngrok.app/roam/webhook
roam: webhook listening on 0.0.0.0:8648/roam/webhook (public: https://….ngrok.app)
✓ roam connected
```

The `org token` line (rather than `PAT owner=…`) confirms the plugin recognized this as an API-key bot rather than a personal-access token.

Then in Roam:

1. **DM the bot.** Open a direct message with the bot in Roam and send it a hello. It should reply with the persona you set.
2. **Post a message in a channel you added it to.** If `ROAM_REQUIRE_MENTION=false`, post any message — the bot replies in a new thread (with `ROAM_REPLY_IN_THREAD=true`). If `ROAM_REQUIRE_MENTION=true`, @-mention the bot to trigger it.
3. **Pull it into a thread.** From another chat the bot isn't a member of, @-mention it inside an existing thread. The bot replies in that same thread.

### Caveats for proactive (every-message) bots

With `ROAM_REQUIRE_MENTION=false`, the bot answers every message in the channel. Two things to watch for:

- **Self-message loop protection comes from the bot's identity.** When the bot posts a reply, Roam delivers that outbound back via the webhook (the bot is technically a participant in its own chat). The plugin filters these out by comparing `userId` against the bot's address ID, fetched at startup via `/v1/token.info`. If that call fails, the log shows `roam: token.info failed: …` — in that state the bot can loop on its own replies once you flip `requireMention` off. Fix the `token.info` issue (check token scopes, approval status, and network reachability) before enabling proactive mode.
- **Every message in the channel triggers an agent run.** If the channel is busy, runs queue up and tokens burn fast. Either narrow which groups the bot listens in (`ROAM_ALLOWED_GROUPS`) or add a `SOUL.md` rule telling the agent when *not* to reply.

### Restricting which users and groups can reach the bot

By default, with `ROAM_ALLOW_ALL_USERS=true`, the bot answers anyone who can reach it. To restrict:

```bash
# DM senders allowlist — Roam user UUIDs, comma-separated.
echo "ROAM_ALLOWED_USERS=<uuid1>,<uuid2>" >> ~/.hermes/profiles/org/.env

# Groups allowlist — Roam chat/group UUIDs, comma-separated.
echo "ROAM_ALLOWED_GROUPS=<chatId1>,<chatId2>" >> ~/.hermes/profiles/org/.env

# Then remove the catch-all:
sed -i '' '/^ROAM_ALLOW_ALL_USERS/d' ~/.hermes/profiles/org/.env

org gateway restart
```

`ROAM_ALLOWED_GROUPS` IDs are the group's chat UUID, found at the bottom of the group's **Group Settings** page in Roam.

## Run additional bots

To add another bot, create another Hermes profile and repeat the Org Bot setup with a new name and a unique webhook port:

```bash
hermes profile create another
# Install the plugin and write .env / config.yaml under
# ~/.hermes/profiles/another/, using ROAM_WEBHOOK_PORT=8649 (or any free port).
another gateway start
```

## Troubleshooting

### "I sent a message and got nothing back"

Tail the gateway log and re-send:

```bash
tail -f ~/.hermes/logs/gateway.log | grep -i roam
# or for a non-default profile:
tail -f ~/.hermes/profiles/<name>/logs/gateway.log | grep -i roam
```

The most common drop reasons are logged explicitly:

- `roam: dropping group message (require_mention=true, no @bot mention)` — flip `ROAM_REQUIRE_MENTION=false`, or @-mention the bot using Roam's autocomplete (so it inserts a rich mention, not a plain string).
- `roam: rejecting unauthorized message chat_type=… chat_id=… user_id=…` — the sender or group isn't on the allowlist. Add them to `ROAM_ALLOWED_USERS` / `ROAM_ALLOWED_GROUPS`, or enable `ROAM_ALLOW_ALL_USERS=true` for dev.
- No log entry at all means the webhook never reached the plugin. Check the tunnel's request inspector (ngrok exposes one at `http://127.0.0.1:4040`) to see whether Roam delivered to the URL.

### `webhook.subscribe failed` at startup

The plugin's auto-subscription call returned a non-2xx. Common causes:

- The URL isn't reachable from Roam's servers (tunnel not running, wrong port).
- The token is missing the scopes required to manage webhook subscriptions.
- The webhook secret is malformed (must be the `whsec_…` value Roam printed when you created the token).

You can subscribe manually if you'd rather skip auto-subscription:

```bash
curl -X POST https://api.ro.am/v1/webhook.subscribe \
  -H "Authorization: Bearer $ROAM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"<your tunnel>/roam/webhook","event":"chat.message"}'
```

### Markdown rendering looks wrong in Roam

Hermes' content generator emits Markdown using the chat-platform convention where a single newline is a hard line break (the same convention used by GFM, Slack, Discord, Telegram). Roam's renderer follows CommonMark's other allowed softbreak rendering — a single newline collapses to a space — so unmodified Hermes output would smush lines together. The plugin reconciles this by inserting blank lines between non-empty lines outside fenced code blocks, which renders correctly under either softbreak choice.

### Two bots with the same display name

If you create both a Personal Bot and an Org Bot and give them the same name in Roam (e.g. both "Hermes"), users can't tell which one they're @-mentioning. Rename one in Roam admin — the display name on the token (Personal) or API client (Org) is what shows in chat.

### Where to file issues

The plugin source lives at [WonderInventions/hermes-roam](https://github.com/WonderInventions/hermes-roam). File issues there for bugs in the Roam integration; for Hermes core issues, use the [Hermes Agent repo](https://github.com/NousResearch/hermes-agent).