?

Overview

The Slack transport (internal/transport/slack/) uses Socket Mode — an outbound WebSocket to Slack — so the daemon needs no public HTTP surface. Inbound events flow over the socket; outbound calls hit the standard Web API (chat.postMessage).

Two tokens

Slack Socket Mode requires two tokens with disjoint responsibilities:

Token Prefix Scope Purpose
App-level token xapp- connections:write Opens the Socket Mode WebSocket.
Bot token xoxb- chat:write + event subscriptions Sends messages, subscribes to events.

App setup

Full step-by-step at https://app.slack.com/apps :

  1. Create a new app ("From scratch"). Pick a workspace.
  2. Enable Socket Mode (Settings → Socket Mode). Generate an app-level token with connections:write. This is the xapp-* token.
  3. Configure event subscriptions (Features → Event Subscriptions). Subscribe to message.channels, message.im, or whichever channel scopes the bot should hear. You do not need a Request URL because Socket Mode delivers events over the socket instead.
  4. Add bot scopes (Features → OAuth & Permissions). Minimum: chat:write. Add im:history, channels:history, groups:history, or mpim:history matching your event subscriptions.
  5. Install the app to the workspace. The install screen hands back the xoxb-* bot token.
  6. Optionally record the bot's own user ID (starts with U…). This is what rousseau uses for own-message loop prevention.

Configuration

slack:
  app_token: "xapp-1-A0..."
  bot_token: "xoxb-1234..."
  bot_user_id: "U0123ABCD"
  reply_header: ""
  allowlist:
    - "U0ALICE"
    - "U0BOB"
Field Default Effect
app_token required xapp-* app-level token with connections:write.
bot_token required xoxb-* bot token with chat:write.
bot_user_id empty Bot user's U… ID for own-message loop prevention. Optional; falls back to inspecting the bot_id field.
reply_header empty Prepended to every outbound message.
allowlist [] Slack user IDs whose messages are handled.

Command-line

rousseau slack \
  --app-token xapp-... \
  --bot-token xoxb-... \
  --bot-user-id U0123ABCD

Wire format

  • Inbound. Slack sends JSON envelopes over the WebSocket. rousseau ACKs the envelope, extracts the message text and sender, and hands it to the handler.
  • Outbound. POST https://slack.com/api/chat.postMessage with {"channel": "<id>", "text": "…"} and Authorization: Bearer <bot_token>.

OAuth scopes explained

Every scope grants a specific API surface. The scopes rousseau needs, and what breaks without them:

Scope Endpoint used Broken without
connections:write apps.connections.open (Socket Mode WebSocket) Cannot open the socket. Required.
chat:write chat.postMessage Cannot reply to any message. Required.
im:history conversations.history for DMs (indirect via events) Bot won't see DM contents in events.
im:read im.list, DM metadata Cannot list open DMs.
im:write conversations.open Cannot open a new DM (only relevant if you want the bot to DM someone unprompted).
mpim:history, channels:history, groups:history Multi-party IMs / channels / private channels Bot won't see message contents outside DMs.

Set the scopes under OAuth & Permissions > Bot Token Scopes. Only add scopes you actually need — Slack shows a warning at install time about each scope, and end users are more likely to install a bot with a narrow permission surface.

Own-message loop prevention

Without protection, a bot that replies to messages will also see its own replies as inbound events — leading to runaway loops. Rousseau handles this via bot_user_id:

// Simplified — actual logic in internal/transport/slack/client.go
if msg.User == cfg.BotUserID {
    continue // Skip: this is our own outbound message echoing back.
}

Retrieve your bot's user ID once via:

curl -H "Authorization: Bearer xoxb-your-token" \
  https://slack.com/api/auth.test

The response includes user_id. Paste it into slack.bot_user_id in config, or pass with --bot-user-id.

Threading

Slack messages carry a thread_ts when they are replies in a thread. Rousseau's outbound calls include thread_ts when the inbound event had one, so bot replies stay threaded. Top-level messages become new threads only when the user starts one.

Failure modes

Symptom Fix
invalid_auth on socket open app_token is wrong or missing connections:write. Regenerate.
Inbound events never arrive Verify Event Subscriptions are enabled and the relevant message.* events are subscribed.
Bot replies to its own messages Set bot_user_id in config.
not_in_channel on send Invite the bot to the channel (/invite @rousseau-bot).
DM works but channel does not Missing channels:history scope, or the bot has not been invited to the channel.

Troubleshooting

invalid_auth on socket open

The xapp-… token is wrong or lost its scope. Regenerate from Basic Information > App-Level Tokens, ensure connections:write is on the new token.

not_authed on chat.postMessage

Bot token (xoxb-…) missing or wrong. Regenerate from OAuth & Permissions > Bot User OAuth Token.

Events arrive but rousseau responds to none of them

Check the allowlist. In pattern mode with default: deny, unlisted users get silently dropped. Look for router.transport.rejected in the logs.

channel_not_found on outbound

The Slack channel ID (C…) has changed — for example, a channel was archived and recreated. Update any hardcoded channel IDs. Rousseau normally uses the channel from the inbound event, so this only happens with cron delivery to a fixed channel.

Bot appears offline in Slack

Socket Mode idles the WebSocket every ~30s. If Slack shows the bot as offline, verify: (1) the daemon is running (systemctl --user status), (2) the WebSocket is connected (log line slack.connected), (3) the machine's clock is within 30s of true time.

Related pages

Further reading

  • internal/transport/slack/client.go — Socket Mode connection, event pump, chat.postMessage.
  • internal/cli/slack.go — CLI wiring.
  • internal/transport/router.go — allowlist enforcement.
  • Slack API docs: Socket Mode.

Type to search 150+ pages. Ranking: BM25 with title/description boost.