Chat surfaces

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 as my-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

Terminal
hubzoid slack manifest my-hub > manifest.json

The 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:write scope and copy the token, which starts with xapp-.

Add the tokens to the Hub

my-hub/.env
SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-...
SLACK_IDENTITY_MAPPING=true

Identity 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

my-hub/channels/slack-assistant.yaml
version: 1
id: slack-assistant
provider: slack_assistant
enabled: true

Start the Hub

Terminal
hubzoid run my-hub

hubzoid 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:

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

WhereHowIdentity surface
Assistant sidebarOpen the app from Slack's sidebar. Suggested prompts come from AGENTS.md, and a "Thinking..." status shows while the agent worksslack-dm
Direct messageMessage the bot directlyslack-dm
Channel or group DM threadInvite the bot and mention it. It answers in the threadslack-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.

  1. Slack event
  2. Slack app (Socket Mode)
  3. Thread read back
  4. Bridge
  5. Streamed reply
The Slack app keeps no conversation store of its own. Slack holds the thread.

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 runsWho the message runs as
Sign-in onThe Hubzoid account with that email, if it is active and not blocked. Otherwise nobody, and the message is refused
Sign-in offThe 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.

SurfaceWho is behind the promptCan be allowed
slack-dmOne person, in a DM or the sidebarYes, with identity mapping
slack-channelEveryone in the thread, answered as the person who mentioned the botNever

To allow restricted tools in DMs, list every surface you want, because the setting replaces the default:

my-hub/.env
HUBZOID_RESTRICTED_SURFACES=web,api,mcp,workflow,slack-dm

Then 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 scopeWhy
app_mentions:readReceive mentions in channels
assistant:writeUse the assistant sidebar, its status line and suggested prompts
channels:history, groups:history, mpim:history, im:historyRead the whole thread in public channels, private channels, group DMs and DMs
chat:write, chat:write.publicPost and edit replies
files:readDownload files people attach
im:read, im:writeReceive and answer direct messages
users:read, users:read.emailLook 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.

  1. Run hubzoid slack manifest my-hub > manifest.json.
  2. In the app's settings open App Manifest, switch the editor to JSON, replace the whole contents and save.
  3. Approve Reinstall to Workspace.
  4. Run hubzoid restart my-hub so 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:

Terminal
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-local answers 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:

Terminal
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 U0123ABCD

Who 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 seeWhat 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 nothingCheck that app_mention and message.im are subscribed, or reapply the manifest
"Sending messages to this app has been turned off" in a DMThe Messages tab is off. Reapply the manifest, or turn it on under App Home
The bot replies with an error mentioning 403No 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 seeWhat to do
missing_scope for channels:history, groups:history or mpim:historyThe app came from an older manifest. Update it and reinstall
The bot replies with an error mentioning 401The bridge started with a different BRIDGE_API_KEYS. Restart the Hub after changing it

Next steps

Choose which cookies Hubzoid can use. You can change this at any time from Cookie settings in the footer. Read the Cookie Policy for details.