Slack
Run the Hub as a Slack bot in the assistant sidebar, direct messages and channel threads, with each sender matched to their access in Settings.
The Slack app lets teammates use the Hub where they already work: in Slack's assistant sidebar, in a direct message, or by mentioning the bot in a channel thread. It connects through Slack's Socket Mode, an outbound connection, so the Hub needs no public address.
By the end of this page you will have a Slack app for your Hub, tokens in its .env, and teammates getting answers in Slack under their own access. The same agent, knowledge and tools serve Slack and the web chat. Only the history and the identity rules differ.
What you need
- A Hub that runs with
hubzoid run, such asmy-hub. - Permission to create an app in your Slack workspace.
- For each person who will write to the bot: a grant of Use this agent, given to the email on their Slack profile. With sign-in on, that email must also have an active account.
Generate the manifest
hubzoid slack manifest my-hub > manifest.jsonThe manifest is JSON (--format yaml for YAML), filled in from the main AGENTS.md: name: becomes the app and bot name, description: the app description, and the first 4 suggestions: the sidebar's suggested prompts. Slack rejects descriptions over 144 characters, and the command warns when yours is longer.
Create the app
Open api.slack.com/apps (opens in a new tab), choose Create New App, then From a manifest. Pick the workspace, paste the contents of manifest.json and create the app.
Install it and copy two tokens
- Install App → Install to Workspace. Copy the Bot User OAuth Token, which starts with
xoxb-. - Basic Information → App-Level Tokens → Generate Token and Scopes. Give it the
connections:writescope and copy the token, which starts withxapp-.
Add the tokens to the Hub
SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-...
SLACK_IDENTITY_MAPPING=trueIdentity mapping matches each sender to a person by their Slack profile email. Without it, every message reaches the Hub without a verified person and is refused.
Turn the Slack channel on
version: 1
id: slack-assistant
provider: slack_assistant
enabled: trueStart the Hub
hubzoid run my-hubhubzoid run starts the Slack app with the Hub, next to the bridge and the web app. You should see a line like this in the output:
→ slack my-hub: Slack app (Socket Mode)If a token is missing, the Slack channel stays off and the start names what is missing, while the rest of the Hub keeps running.
Try it
Open the bot from the Apps section of Slack's sidebar and choose a suggested prompt. Send it a direct message. Invite it to a channel with /invite @your-bot and mention it. Each person who writes needs Use this agent on the agent's Access tab.
Where people reach the bot
| Where | How | Identity surface |
|---|---|---|
| Assistant sidebar | Open the app from Slack's sidebar. Suggested prompts come from AGENTS.md, and 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 answers only when mentioned. On every turn it reads the whole thread: its own messages become assistant turns and everyone else's become user turns. In a channel thread that means several people's messages form one prompt, answered as the person who mentioned the bot.
How replies and attached files are handled
Replies stream into a placeholder that is edited about every 0.75 seconds. Hubzoid converts the answer to Slack formatting, wraps tables in code blocks and asks the model to prefer lists. A reply longer than 3,500 characters is cut with a note inviting the person to ask for the rest. Tool activity is not shown with the default SHOW_TOOLS=compact, and reasoning is never posted.
Files attached in a thread are downloaded with the bot token into that thread's 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. Files over HUBZOID_MAX_UPLOAD_BYTES (25 MiB by default) are skipped.
How identity works
The Slack app is a thin client of the Hub's bridge. For every message it reads the Slack thread back and sends the conversation to the bridge on 127.0.0.1, with the sender's identity signed by the deployment key. The bridge trusts an identity only with a valid signature.
- Slack event
- Slack app (Socket Mode)
- Thread read back
- Bridge
- Streamed reply
For each sender the app asks Slack for the email on their profile, which needs the users:read.email scope the manifest requests:
| How the Hub runs | Who the message runs as |
|---|---|
| Sign-in on | The Hubzoid account with that email, if it is active and not blocked. Otherwise nobody, and the message is refused |
| Sign-in off | The email as it is, because there are no accounts to match |
Emails are compared without regard to case. If the Slack lookup fails, the message runs as nobody. The account check runs on every message, so blocking or deleting someone takes effect at once. Slack usage and tool decisions are recorded under that person in Settings.
Restricted tools in Slack
Neither Slack surface may reach restricted tools by default. The default list is web,api,mcp,workflow.
| Surface | Who is behind the prompt | Can be allowed |
|---|---|---|
slack-dm | One person, in a DM or the sidebar | Yes, with identity mapping |
slack-channel | Everyone in the thread, answered as the person who mentioned the bot | Never |
To allow restricted tools in DMs, list every surface you want, because the setting replaces the default:
HUBZOID_RESTRICTED_SURFACES=web,api,mcp,workflow,slack-dmThen grant the person the capability and restart the Hub.
Never add slack-channel
In a channel thread, everyone's text becomes one prompt, but the tool runs with the permissions of whoever mentioned the bot. Anyone in the thread could steer a restricted tool only that person may use.
What the manifest requests
These are the permissions and events the generated manifest asks Slack for.
Every scope and event the manifest requests
| Bot scope | Why |
|---|---|
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 |
im:read, im:write | Receive and answer direct messages |
users:read, users:read.email | Look up a sender's profile email |
The manifest subscribes to app_mention, assistant_thread_started, assistant_thread_context_changed and message.im, enables Socket Mode, and turns on the Messages tab so people can send direct messages.
Update an existing app
When a Hubzoid release changes the manifest, update the app in place. The tokens stay the same.
- Run
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.
- Approve Reinstall to Workspace.
- Run
hubzoid restart my-hubso the Slack app picks up the new permissions.
Run it for a team
The Slack app runs inside its Hub. hubzoid run, or the one service that runs your deployment, starts it with the Hub and restarts it with the Hub. Do not also run hubzoid slack run next to it, or it runs twice. Its output is in the Hub's logs:
hubzoid logs my-hub -f- One Slack app serves one Hub. Each Hub that should be in Slack gets its own app and tokens.
- A Hub on
MODEL=claude-localanswers Slack with the Claude subscription signed in on the host. For many people at once, use a provider key. See Agents and models. - Limit reach with Slack itself: invite the bot only to the channels that need it.
Slack threads that start work
Hubzoid also has Slack connections that turn a mention into tracked work, with people linked by their Slack user id instead of their email. A connection is a channels/<id>.yaml file with provider: slack, managed with hubzoid channels:
hubzoid channels validate my-hub --file my-hub/channels/slack-main.yaml
hubzoid channels apply my-hub --file my-hub/channels/slack-main.yaml --expected-revision absent
hubzoid channels slack manifest my-hub slack-main --output slack-app-manifest.json
hubzoid channels link my-hub slack-main --external-id U0123ABCD --account priya@example.com
hubzoid channels doctor my-hub slack-main --actor U0123ABCDWho may start work from it is the Start work from grant on the agent's Access tab. hubzoid channels slack import-legacy writes a disabled connection file for the setup on this page. Once a Slack connection is enabled, hubzoid slack run refuses to start, so one Slack app never has two listeners. See the CLI reference.
If something goes wrong
| What you see | What to do |
|---|---|
Slack adapter cannot start: SLACK_BOT_TOKEN, SLACK_APP_TOKEN not set in .env. | Add both tokens to <hub>/.env |
An error saying SLACK_BOT_TOKEN should start with xoxb- | The app token is in the bot token line. Swap them |
| The app connects but mentions do nothing | Check that app_mention and message.im are subscribed, or reapply the manifest |
| "Sending messages to this app has been turned off" in a DM | The Messages tab is off. Reapply the manifest, or turn it on under App Home |
| The bot replies with an error mentioning 403 | No person who may use the agent was found. Turn on SLACK_IDENTITY_MAPPING, check the sender's Slack email has an active account, and grant it Use this agent |
More problems and fixes
| What you see | What to do |
|---|---|
missing_scope for channels:history, groups:history or mpim:history | The app came from an older manifest. Update it and reinstall |
| The bot replies with an error mentioning 401 | The bridge started with a different BRIDGE_API_KEYS. Restart the Hub after changing it |

