---
title: Slack
description: Run the Hub as a Slack bot over Socket Mode, with the app manifest, tokens, DMs and channel threads, identity mapping and restricted tools in Slack.
canonical_url: https://hubzoid.com/docs/chat/slack
last_updated: 2026-09-27
---

# Slack

Run the Hub as a Slack bot over Socket Mode, with the app manifest, tokens, DMs and channel threads, identity mapping and restricted tools in Slack.

The Slack adapter lets teammates use the Hub from Slack: in the assistant sidebar, in a direct message, or by mentioning the bot in a channel thread. It connects through Slack's Socket Mode, an outbound WebSocket, so the hub needs no public URL.

## When to use this

Use Slack when the team already works there and the Hub should answer where questions come up. The same agent, knowledge, skills and tools serve Slack and the [web chat](https://hubzoid.com/docs/chat/web-chat). Only the history and the identity rules differ.

## How it works

The adapter is a thin client of the hub's bridge. For every message it reads the Slack thread back, sends the conversation to the bridge's [OpenAI-compatible API](https://hubzoid.com/docs/chat/openai-compatible-api) on `127.0.0.1`, and streams the answer into Slack by editing a placeholder message.

Slack event → Adapter (Socket Mode) → Thread read back → Bridge /v1 → Streamed reply

The adapter keeps no conversation store of its own. Slack holds the thread.

The adapter authenticates to the bridge with the first key in `BRIDGE_API_KEYS`, read from the same `.env` as the bridge.

## Set up the Slack app

1. **Generate the manifest**

   ```bash
   hubzoid slack manifest my-hub > manifest.json
   ```

   The manifest is JSON by default (`--format yaml` for YAML). It is pre-filled from the main `AGENTS.md`: the `name:` becomes the app and bot name, the `description:` becomes the app description, and the first 4 `suggestions:` become the sidebar's suggested prompts. Slack rejects descriptions over 144 characters, and the command warns when yours is longer.

2. **Create the app from the manifest**

   Open [api.slack.com/apps](https://api.slack.com/apps), choose **Create New App**, then **From a manifest**. Pick the workspace, paste the contents of `manifest.json`, and create the app.

3. **Install it and copy two tokens**

   - **Install App**, then **Install to Workspace**. Copy the **Bot User OAuth Token**, which starts with `xoxb-`.
   - **Basic Information**, then **App-Level Tokens**, then **Generate Token and Scopes**. Give it the `connections:write` scope and copy the token, which starts with `xapp-`.

4. **Add the tokens to the hub**

   ```bash title="my-hub/.env"
   SLACK_BOT_TOKEN=xoxb-...
   SLACK_APP_TOKEN=xapp-...
   ```

5. **Start the adapter**

   ```bash
   hubzoid run my-hub --slack
   ```

   `--slack` (or `-s`) starts the adapter next to the bridge and chat app. You should see `slack starting (Socket Mode)` in the output.

6. **Try the three entry points**

   Open the bot from the Apps section of Slack's sidebar and click a suggested prompt. Send the bot a direct message. Invite it to a channel with `/invite @your-bot` and mention it.

## What the manifest requests

| Bot scope | Why the adapter needs it |
| --- | --- |
| `app_mentions:read` | Receive mentions in channels. |
| `assistant:write` | Use the assistant sidebar, its status line and suggested prompts. |
| `channels:history`, `groups:history`, `mpim:history`, `im:history` | Read the whole thread in public channels, private channels, group DMs and DMs. |
| `chat:write`, `chat:write.public` | Post and edit replies. |
| `files:read` | Download files people attach in a thread. |
| `im:read`, `im:write` | Receive and answer direct messages. |
| `users:read`, `users:read.email` | Look up a sender's profile email for [identity mapping](https://hubzoid.com/docs/chat/slack#identity-mapping). |

The manifest subscribes to the `app_mention`, `assistant_thread_started`, `assistant_thread_context_changed` and `message.im` events, enables Socket Mode and interactivity, and turns on the app's Messages tab so people can send it direct messages. Token rotation is off.

## Run it for a team

Two layouts work. Pick by how you want restarts to behave.

### Separate process

The adapter restarts on its own, and a Slack problem never takes the chat app down.

```bash
hubzoid run my-hub          # terminal A, or the hub's own service
hubzoid slack run my-hub    # terminal B
```

For systemd, print a unit and install it:

```bash
hubzoid slack systemd my-hub > /etc/systemd/system/hubzoid-slack@my-hub.service
systemctl daemon-reload
systemctl enable --now hubzoid-slack@my-hub.service
```

The unit reads `EnvironmentFile=<hub>/.env`, runs as the `hubzoid` user (change it with `--user`), and declares `Requires=hubzoid@my-hub.service`, so it starts only after the hub's own service. `--python` sets the interpreter path when it differs from the one running the command.

### Inline with the bridge

One process tree and one log. Add `--slack` to the command your service runs:

```ini
ExecStart=/opt/hubzoid/.venv/bin/hubzoid run %i --slack
```

A crash on the Slack side stops the whole unit, and systemd restarts everything together.

The two commands treat bad tokens differently. `hubzoid run --slack` prints a warning, skips Slack and keeps the bridge and chat app running. `hubzoid slack run` exits with the error. Both check that the bot token starts with `xoxb-` and the app token with `xapp-`, which catches tokens pasted into the wrong line.

## DMs, channels and the assistant sidebar

| Where | How people reach the bot | Identity surface |
| --- | --- | --- |
| Assistant sidebar | Open the app from Slack's sidebar. Suggested prompts come from `AGENTS.md`. A "Thinking..." status shows while the agent works. | `slack-dm` |
| Direct message | Message the bot directly. | `slack-dm` |
| Channel or group DM thread | Invite the bot and mention it. It answers in the thread. | `slack-channel` |

In channels the bot responds only when mentioned. On every turn it reads the entire thread: its own messages become assistant turns, everyone else's become user turns, and the mention itself is stripped. In a channel thread that means several people's messages form one prompt.

Replies stream into a placeholder that is edited about every 0.75 seconds. Hubzoid converts the answer to Slack formatting, wraps Markdown tables in code blocks and asks the model to prefer lists, because Slack does not render tables. A reply longer than 3,500 characters is cut with a note inviting the person to ask for the rest. With the default `SHOW_TOOLS=compact`, tool activity is not shown in Slack. Reasoning is never posted: while a `claude-local` model is thinking, the placeholder shows a "Thinking" indicator instead.

Files attached in a thread are downloaded with the bot token and stored in that thread's uploads folder, `<hub>/.hubzoid/chats/slack-<channel>-<thread_ts>/uploads/`. Images are shown to the model and other files are read with `read_upload`, as in the web chat. Each file is downloaded once per thread, and files over `HUBZOID_MAX_UPLOAD_BYTES` (25 MiB by default) are skipped. Files the agent writes in a thread land in the same thread's folder.

## Identity mapping

By default the adapter tells the bridge nothing about who is writing, and every Slack message runs without a user. On a hub whose access still comes from chat app groups, Slack is then the only boundary: anyone who can reach the bot can use the Hub's unrestricted tools and knowledge.

Turn on identity mapping to recognize people:

```bash title="my-hub/.env"
SLACK_IDENTITY_MAPPING=true
```

For each sender, the adapter asks Slack for the email on their profile (`users.info`, which needs the `users:read.email` scope the manifest requests) and forwards it to the bridge. The bridge resolves that email the same way it resolves a signed-in web chat user, looking up their groups and grants. Emails are compared without regard to case. If the lookup fails, the message runs without an identity. An email that matches no account brings no groups or grants with it. Successful lookups are cached for the life of the adapter process.

Identity mapping matters in three places:

- **Hubs managed in the Console.** Once a hub's access is managed in [the Console](https://hubzoid.com/docs/console/agents-and-access), every chat request must carry a verified email that holds **Use this agent**. Without identity mapping the bridge refuses Slack messages to such a hub.
- **Restricted tools.** A restricted tool needs a person behind the request. See the next section.
- **Usage.** The Console attributes Slack conversations to the person instead of an anonymous sender.

For the email to match, a person's Slack profile email must be the same address they use to sign in to the chat app or that appears in the hub's grants.

## Restricted tools in Slack

Slack arrives at the bridge as two different surfaces, and neither may reach [restricted tools](https://hubzoid.com/docs/guides/restrict-tools) by default. The default set of surfaces that may reach them is `owui,web,api,mcp,workflow`.

| Surface | Who is behind the prompt | Can be opted in |
| --- | --- | --- |
| `slack-dm` | One person, in a DM or the assistant sidebar. | Yes, together with identity mapping. |
| `slack-channel` | Everyone who wrote in the thread, answered under the identity of the person who mentioned the bot. | Never. |

To allow restricted tools in DMs, list every surface you want, because the setting replaces the default rather than adding to it:

```bash title="my-hub/.env"
SLACK_IDENTITY_MAPPING=true
HUBZOID_RESTRICTED_SURFACES=owui,web,api,mcp,workflow,slack-dm
```

Then give the person the matching permission, in the Console or through their group, and restart the hub so the bridge reads the new list.

> **Caution: Never add slack-channel**
>
> In a channel thread, the text of every participant is flattened into one prompt, but the tool runs with the permissions of whoever mentioned the bot. Anyone in the thread could steer a restricted tool that only the mentioning person is allowed to use. Keep `slack-channel` out of `HUBZOID_RESTRICTED_SURFACES`.

Every restricted tool decision on Slack is recorded with its surface, so you can review it with `hubzoid audit my-hub` or on the Console's Activity page.

## Update an existing app

When a Hubzoid release changes the manifest (a new scope or event), update the app in place. Tokens survive.

```bash
hubzoid slack manifest my-hub > manifest.json
```

In the app's settings open **App Manifest**, switch the editor to **JSON**, replace the whole contents and save. Slack asks you to **Reinstall to Workspace**. Approve it, then restart the adapter so it picks up the new permissions. The bot and app tokens stay the same, so `.env` needs no change.

## Troubleshooting

| Symptom | Likely cause |
| --- | --- |
| `Slack adapter cannot start: SLACK_BOT_TOKEN, SLACK_APP_TOKEN not set in .env.` | The tokens are missing from `<hub>/.env`. |
| An error saying `SLACK_BOT_TOKEN` should start with `xoxb-` | The app token was pasted into the bot token line. Swap them. |
| The adapter connects but mentions do nothing | Event subscriptions were edited by hand. Check that `app_mention` and `message.im` are subscribed, or reapply the manifest. |
| `missing_scope` for `channels:history`, `groups:history` or `mpim:history` | The app was installed from an older manifest. Update it and reinstall. |
| "Sending messages to this app has been turned off" in a DM | The Messages tab is off. Reapply the manifest, or enable it under **App Home**. |
| The bot replies with an HTTP 401 error | The bridge was started with a different `BRIDGE_API_KEYS` value. Restart the bridge after changing it. |
| The bot replies with an HTTP 403 error | The hub is managed in the Console and the sender has no identity or no **Use this agent** grant. Turn on identity mapping and check the grant. |

## Operational notes

- One Slack app serves one hub. Each hub that should be in Slack gets its own app and tokens.
- A hub on `MODEL=claude-local` answers Slack with the Claude subscription signed in on the host. For many concurrent Slack users, use a provider key instead. See [Agents and models](https://hubzoid.com/docs/concepts/agents-and-models).
- Limit reach with Slack itself: invite the bot only to the channels that need it and rely on your workspace's channel and guest policies.

## Next steps

- [Identity and access](https://hubzoid.com/docs/concepts/identity-and-access): How surfaces, identities, grants and restricted tools fit together.
- [Restrict tools](https://hubzoid.com/docs/guides/restrict-tools): Put a sensitive tool behind a permission and test it.
- [WhatsApp and Telegram](https://hubzoid.com/docs/chat/whatsapp-and-telegram): Reach teammates on their phones with an allowlist of known numbers.
