openclaw.connect('slack')
/** Your AI teammate available 24/7 in every Slack workspace channel */
## Step 1: Create a Slack App
## Step 2: Configure OpenClaw
// π‘ Tip: If you don't have a public URL, use ngrok or Cloudflare Tunnel to expose port 3000
π¬ Example Messages
β’ @alice shipped the auth refactor β merged to main
β’ @bob: DB migration tested, deploying at 2pm
β’ @carlos: blocked on design review for new dashboard
β Blocker: @carlos needs design sign-off before EOD
Picking a transport before you build anything
This is the first decision and the expensive one to reverse, because the Slack app manifest differs between the two. Both modes reach parity for messaging and slash commands, so the question is purely about your network and how many gateways you run.
| Your situation | Transport | Reasoning |
|---|---|---|
| One gateway on a laptop or home server | Socket Mode | No inbound port, no public hostname, no TLS certificate to maintain. |
| Corporate network blocks inbound HTTPS | Socket Mode | Only needs outbound WSS to wss-primary.slack.com. |
| Several gateway replicas behind a load balancer | HTTP | Socket Mode ties an app-level token to a connection; splitting it across replicas is not the intended shape. |
| Outbound WSS is blocked by policy | HTTP | The one case where Socket Mode is simply unavailable to you. |
| You already terminate HTTPS for other services | HTTP | Marginal, but you avoid a long-lived websocket you have to keep alive. |
Socket Mode is the right default for a self-hosted agent. It gives you a Slack integration with no public attack surface at all β which matters more here than for an ordinary bot, because the thing on the other end of the socket can run shell commands.
Socket Mode, start to finish
Two tokens are involved and they are easy to confuse. The app-level token authorises the websocket itself; the bot token authorises actions once messages are flowing. Getting one right and the other wrong produces a connection that establishes and then does nothing.
# api.slack.com/apps β your app β Basic Information # Generate an App-Level Token with scope: connections:write # Then: OAuth & Permissions β install to workspace β copy the Bot User OAuth Token
The `connections:write` scope is what makes it an app-level token rather than a bot token. If Slack does not offer you Socket Mode, this scope is usually why.
{
channels: {
slack: {
mode: "socket",
appToken: { source: "env", id: "SLACK_APP_TOKEN" },
botToken: { source: "env", id: "SLACK_BOT_TOKEN" },
},
},
}Both are SecretRefs. Slack tokens are bearer credentials with no IP binding, so a leaked one is usable by anyone until you rotate it β keep them out of the config file.
# Minimum bot scopes: # app_mentions:read channels:history channels:read # chat:write commands groups:history # groups:read im:history im:read # users:read
This list is the floor, not a suggestion. Missing `channels:history` in particular produces a bot that can post but cannot read, which reads like the agent ignoring context rather than a permissions problem.
openclaw config patch --file ./slack.socket.json5 openclaw gateway restart openclaw gateway status
`gateway status` is the check that matters. A misconfigured app-level token fails at connect time, and that failure is visible here rather than in Slack.
HTTP mode, and the ID trap that catches everyone
If you have concluded you need HTTP mode, the setup differs in two places: a signing secret replaces the app-level token, and Slack needs a URL it can actually reach. Everything downstream behaves identically.
{
channels: {
slack: {
mode: "http",
botToken: { source: "env", id: "SLACK_BOT_TOKEN" },
signingSecret: { source: "env", id: "SLACK_SIGNING_SECRET" },
webhookPath: "/slack/events",
},
},
}The signing secret is how you verify a request genuinely came from Slack. Without verification, your webhook path is an unauthenticated endpoint that instructs an agent β treat skipping this as equivalent to leaving the gateway open.
# Channel allowlists are keyed by Slack ID, never by name. # Right-click a channel β View channel details β the ID is at the bottom. # # "C12345678" correct # "#engineering" silently matches nothing
Channel allowlists keyed by `#name` do not error. They match nothing, so the agent stays silent in exactly the channel you meant to enable. This is the single most common Slack configuration mistake.
Worth deciding before this reaches a shared workspace
- βA bot with channels:history in a workspace channel reads everything posted there, including messages sent long before anyone thought about the agent.
- βDM pairing defaults to requiring approval. Setting dmPolicy to "open" with allowFrom: ["*"] means anyone in the workspace gets a private channel to an agent that can run commands.
- βSlack workspaces usually contain other people's data. "Self-hosted so it is private" stops being true the moment the agent is reading a shared channel.
- βGroup DMs are off unless you set dm.groupEnabled β worth knowing before you conclude the integration is broken.