# Connecting Slack

> Create the Wheems Slack app from its manifest, connect its two tokens, allow people, bind channels and test it end to end.

You talk to Wheems through Slack. By the end of this page you will have a Slack app called Wheems in your Slack workspace, its two tokens saved on your Mac, yourself on the list of people allowed to talk to agents, and a chief of staff answering you in Slack.

It takes about ten minutes, most of it in Slack's app settings. You do it once: one Slack app serves every Wheems workspace.

> **Two kinds of workspace:** Slack and Wheems both use the word *workspace*. A **Slack workspace** is your Slack team, such as `acme.slack.com`. A **Wheems workspace** is one business or area of your life, with its own chief of staff and knowledge base. On this page, "workspace" on its own means a Wheems workspace.

## Before you start

You need:

- Wheems installed, with `wheems doctor` passing its System and Herdr core sections. See [Installation](https://wheems.dev/docs/installation/).
- At least one engine installed and signed in (Claude Code, Codex or opencode). `wheems setup` → **Engines** does this. See [Engines and accounts](https://wheems.dev/docs/engines/).
- A Slack workspace where you're allowed to add apps. On some company Slack workspaces only admins can, or apps need an admin's approval. A free Slack workspace of your own works well.
- The Slack desktop app or [slack.com](https://slack.com) in a browser, signed in to that Slack workspace.

## How Wheems talks to Slack

Wheems connects to Slack with **Socket Mode**. The Wheems service on your Mac opens a connection out to Slack, and Slack sends messages down it. Nothing connects in to your Mac, so you need no public address, no domain and no open port.

That needs two tokens, and Wheems checks both with Slack before saving them:

| Token | Looks like | Where you get it | What Wheems uses it for |
| --- | --- | --- | --- |
| **Bot User OAuth Token** | `xoxb-…` | **OAuth & Permissions**, after installing the app | Everything the bot *does*: posting answers, adding and removing the 👀 reaction, finding channels, looking up members |
| **App-Level Token** | `xapp-…` | **Basic Information → App-Level Tokens**, with the `connections:write` scope | Opening the Socket Mode connection that *receives* messages |

The **bot** is the app's user in Slack. You message it directly, or invite it to a channel. Its answers appear under the bot's name, or under your workspace's own name and icon (see [Identities](#identities-how-posts-appear)).

## Connect Slack, step by step

1. **Open setup and choose Channels.**

   ```bash title="Terminal"
   wheems setup
   ```

   Pick **Channels** with the arrow keys and Enter, then **Slack**. Esc or Ctrl+C backs out of any step without saving it.

   Setup writes the app's manifest to `~/wheems/.wheems/slack-app-manifest.yaml`, copies it to your clipboard, and shows this:

   ```text title="Output"
   Create the Slack app
   One Slack app serves every workspace, so you do this once.

   1. At https://api.slack.com/apps?new_app=1, choose "From a manifest" and pick your Slack workspace.
   2. Paste the manifest: it is on your clipboard.
   3. Install the app to the workspace. Under OAuth & Permissions, copy the
      Bot User OAuth Token (xoxb-…).
   4. Under Basic Information → App-Level Tokens, generate a token with the
      connections:write scope (xapp-…).

   It uses Socket Mode, so it needs no public address.
   ```

   Say yes to **Open the Slack app page in your browser?**, or open [api.slack.com/apps](https://api.slack.com/apps?new_app=1) yourself. Leave setup waiting in the terminal.

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

   On Slack's page, choose **Create New App** if asked, then **From a manifest**. Pick the Slack workspace to install it in and choose **Next**.

   Choose the **YAML** tab, delete what's there, and paste the manifest. If it isn't on your clipboard, print it:

   ```bash title="Terminal"
   wheems slack manifest
   ```

   Choose **Next**, check the summary, then **Create**. The manifest is explained line by line [below](#the-manifest-line-by-line).

   > **Another name:** The app is called **Wheems**. To call it something else, change the two `Wheems` values (`display_information.name` and `bot_user.display_name`) before you paste. Nothing in Wheems depends on the name.

3. **Install the app and copy the bot token.**

   In the app's settings, open **OAuth & Permissions** (or **Install App**) and choose **Install to** your Slack workspace. Slack lists what the app may do; choose **Allow**.

   Back on **OAuth & Permissions**, copy the **Bot User OAuth Token**. It starts with `xoxb-`.

4. **Create the app-level token.**

   Open **Basic Information**, scroll to **App-Level Tokens** and choose **Generate Token and Scopes**. Give it any name, such as `socket`, choose **Add Scope**, pick `connections:write`, then **Generate**. Copy the token. It starts with `xapp-`.

5. **Paste both tokens into setup.**

   Back in the terminal, setup asks for the **Bot User OAuth Token**, then the **App-Level Token**. They're masked as you type. Setup checks them with Slack:

   ```text title="Output"
   ◇  Connected as wheems in Acme
   ```

   The first name is the bot's Slack username, the second your Slack workspace. If Slack refuses a token, setup says which and why, and offers to try again. See [Troubleshooting](#troubleshooting).

6. **Say who may talk to your agents.**

   Only people on the allowed list can talk to agents. Messages from anyone else are ignored. Setup asks for **Your Slack member ID**:

   ```text title="Output"
   ◇  Your Slack member ID
   │  U012ABCDEF
   ```

   To find yours, see [Finding a member ID](#finding-a-member-id). Setup looks you up in Slack and shows your name:

   ```text title="Output"
   ◇  Allowed: Alex Example
   ◆  Slack is connected. Its tokens are saved privately in ~/.config/wheems/secrets.json
   ```

   Setup has now written the tokens to the secrets file and your member ID to `install.yaml`:

   ```yaml title="~/wheems/install.yaml"
   connections:
     slack:
       driver: slack
       secret: slack
       allowed_users: [U012ABCDEF]
   ```

7. **Create a workspace that chats in Slack.**

   Setup moves on to **Workspaces**. Give it an id such as `personal`, a name, then choose where you'll chat with it:

   - **The app's direct messages**: private between you and the bot. Only one Wheems workspace can have them.
   - **A channel**: give its name, such as `acme-chat`. Every other workspace chats in a channel.

   Then pick the engine and model for its chief of staff. Setup offers to open the engine in the new folder so you can trust it: say yes, answer the engine's trust question, then press Ctrl+C twice to come back. See [Quick start](https://wheems.dev/docs/quick-start/) for this step in full.

8. **If you chose a channel, invite the bot to it.**

   Create the channel in Slack if it doesn't exist, then type this in the channel:

   ```text title="Slack"
   /invite @Wheems
   ```

   The bot must be a member of the channel. In a public channel it can *see* the channel without being a member, but Slack only sends it messages from channels it's in.

9. **Start the service, or restart it.**

   In setup, choose **Service**. It installs the service under launchd and starts it. If it's already running, setup offers to restart it with the new config. Or from the terminal:

   ```bash title="Terminal"
   wheems service install    # the first time
   wheems service restart    # after any change to Slack, channels or allowed users
   ```

   The service reads the tokens, `install.yaml` and every `workspace.yaml` when it starts. Restart it after changing any of them.

10. **Say hello.**

    In Slack, open the bot under **Apps** in the sidebar and use its **Messages** tab, or post in the workspace's channel. Within a few seconds a 👀 reaction appears on your message: it has reached the agent. The answer arrives in the message's thread, and the 👀 goes.

## The manifest, line by line

`wheems slack manifest` prints the manifest. It's the same one setup copies to your clipboard and saves to `~/wheems/.wheems/slack-app-manifest.yaml`:

```yaml title="Slack app manifest"
display_information:
  name: Wheems
  description: Your agents' chief of staff, from Wheems.
  background_color: "#2c2d30"
features:
  app_home:
    home_tab_enabled: false
    messages_tab_enabled: true
    messages_tab_read_only_enabled: false
  bot_user:
    display_name: Wheems
    always_online: true
oauth_config:
  scopes:
    bot:
      - chat:write
      - chat:write.customize
      - im:history
      - im:read
      - im:write
      - channels:history
      - channels:read
      - groups:history
      - groups:read
      - reactions:write
      - users:read
settings:
  event_subscriptions:
    bot_events:
      - message.im
      - message.channels
      - message.groups
  interactivity:
    is_enabled: true
  org_deploy_enabled: false
  socket_mode_enabled: true
  token_rotation_enabled: false
```

### Bot token scopes

| Scope | Why Wheems needs it |
| --- | --- |
| `chat:write` | Post the agent's answers and Wheems's notices in threads. |
| `chat:write.customize` | Post under a workspace's own name and icon (`identity` in `workspace.yaml`). |
| `im:history` | Read the messages you send the bot directly. |
| `im:read` | See the direct-message conversation with the bot. |
| `im:write` | Reply in that direct-message conversation. |
| `channels:history` | Read messages in public channels the bot has been invited to. |
| `channels:read` | Find a public channel's id from its name, as written in `workspace.yaml`. |
| `groups:history` | Read messages in private channels the bot has been invited to. |
| `groups:read` | Find a private channel's id from its name. |
| `reactions:write` | Add the 👀 reaction when your message reaches the agent, and remove it when the agent answers. |
| `users:read` | Look up a member ID during setup, to show you whose it is. |

### Events

| Event | What it delivers |
| --- | --- |
| `message.im` | Messages you send the bot directly. |
| `message.channels` | Messages in public channels the bot is a member of. |
| `message.groups` | Messages in private channels the bot is a member of. |

Wheems acts only on plain messages from people. Edits, deletions, joins, other bots' posts and its own posts are ignored.

### Settings

| Setting | Value | Why |
| --- | --- | --- |
| `socket_mode_enabled` | `true` | Messages arrive over a connection the service opens, so your Mac needs no public address. |
| `messages_tab_enabled` | `true` | You can message the bot directly from its **Messages** tab. |
| `messages_tab_read_only_enabled` | `false` | You can type in that tab, not only read it. |
| `home_tab_enabled` | `false` | Wheems has no Home tab. |
| `always_online` | `true` | The bot shows as active. |
| `interactivity.is_enabled` | `true` | For approval buttons, which aren't built yet (milestone 3). Nothing uses it today. |
| `token_rotation_enabled` | `false` | The bot token doesn't expire, so the service keeps working without refreshing it. |
| `org_deploy_enabled` | `false` | The app is for one Slack workspace, not a whole Enterprise Grid organisation. |

## Where the tokens are kept

Setup writes the tokens to a secrets file outside your install folder, so they're never committed and never part of what an agent is given:

```json title="~/.config/wheems/secrets.json"
{
  "slack": {
    "bot_token": "xoxb-…",
    "app_token": "xapp-…"
  }
}
```

- The file is readable only by you (mode 600), in a folder only you can open (mode 700).
- It lives in `$XDG_CONFIG_HOME/wheems/` when `XDG_CONFIG_HOME` is set. Set `WHEEMS_SECRETS` to use another file.
- `slack` is the entry's name. `secret: slack` in `install.yaml` points the connection at it; without `secret`, the connection's own name is used.
- Tokens never pass through an agent: you type them into setup, and only the service reads them. They aren't in any agent's instructions, notes or knowledge base.

> **Not a lock against agents:** Mode 600 keeps the file from other users on your Mac, not from programs running as you. Agents run as you, so an agent that went looking, especially one on `permissions: skip`, could read it. Keeping secrets where agents can't reach them comes with the vault (milestone 9) and permission rules (milestone 6).

> **Keep them secret:** Anyone with both tokens can read every message the bot can see and post as it. Don't paste them into Slack, an agent's session, a note or a repository. Wheems refuses notes and skills that look like they hold a token, but it can't stop you pasting one elsewhere.

## Rotating the tokens

Rotate a token when it may have leaked, or when someone who had it leaves.

1. **Get new tokens from Slack.**

   - **Bot token**: on **OAuth & Permissions**, choose **Revoke All OAuth Tokens** if the old one may have leaked, then **Reinstall to** your Slack workspace. Copy the new `xoxb-…` token.
   - **App-level token**: on **Basic Information → App-Level Tokens**, generate a new token with `connections:write`. Then open the old token and **Revoke** it.

2. **Give them to Wheems.**

   ```bash title="Terminal"
   wheems setup
   ```

   Choose **Channels**, then **Slack**. Setup checks the saved tokens. If they still work it asks **Keep using wheems in Acme?**: answer no. If they don't, it says so and asks for new ones straight away. Paste the new tokens.

   If the new tokens are for a different Slack workspace, setup checks that everyone on the allowed list is a member there, and drops anyone Slack says isn't.

3. **Restart the service**, which reads the tokens only when it starts:

   ```bash title="Terminal"
   wheems service restart
   ```

4. **Check**:

   ```bash title="Terminal"
   wheems doctor --quick
   ```

   ```text title="Output"
   Channels
     ✓ slack: Slack: Signed in as wheems in Acme
   ```

## Allowing people

`allowed_users` in `install.yaml` lists the Slack member IDs that may talk to agents. Everyone else is ignored: no reaction, no answer. The service log records each ignored message:

```text title="~/wheems/.wheems/logs/service.log"
2026-11-13T09:12:04.512Z Ignored a message on slack from U034GHIJKL, who is not in allowed_users
```

### Finding a member ID

A member ID starts with `U` or `W`, then letters and digits, such as `U012ABCDEF`. It isn't your display name or email.

1. In Slack, select your profile picture, then **Profile**. For someone else, select their name.
2. Choose **⋯** (More).
3. Choose **Copy member ID**.

### Adding more people

Setup asks for one member ID, the first time. To allow more people, edit the list by hand, then restart the service:

```yaml title="~/wheems/install.yaml" {5}
connections:
  slack:
    driver: slack
    secret: slack
    allowed_users: [U012ABCDEF, U034GHIJKL]
```

```bash title="Terminal"
wheems validate
wheems service restart
```

Everyone on the list can talk to every workspace's chief of staff. Wheems is built for one person today; roles for several people per workspace are planned for after version one.

## Binding channels

Each workspace's `workspace.yaml` says where you chat with its chief of staff, in `channels.chat`:

```yaml title="~/wheems/workspaces/personal/workspace.yaml"
channels:
  chat: { connection: slack, direct: true }
```

```yaml title="~/wheems/workspaces/acme/workspace.yaml"
channels:
  chat: { connection: slack, channel: acme-chat }
```

- **`direct: true`** means the bot's direct messages. Only one workspace per Slack app can have them. `wheems init` gives them to the first workspace, and refuses a second without `--channel`.
- **`channel`** takes a name (`acme-chat` or `#acme-chat`) or a channel id (`C0123ABCD` for public, `G0123ABCD` for private). An id keeps working if the channel is renamed.
- Public and private channels both work. Invite the bot to either: `/invite @Wheems`.
- The service finds each channel when it starts. If it can't find one, it logs this and skips the workspace until the next start:

  ```text title="~/wheems/.wheems/logs/service.log"
  Workspace acme: the bot cannot see channel acme-chat; invite it to the channel
  ```

  When it does find it:

  ```text title="~/wheems/.wheems/logs/service.log"
  Workspace acme: chat on slack C0123ABCD
  ```

- In a bound channel, **every** new message from an allowed person starts a conversation. You don't need to mention the bot. Keep the channel for talking to the chief of staff.

To move a workspace to another channel, change `channels.chat`, invite the bot there, and restart the service.

> **Planned · milestone 3:** `workspace.yaml` already accepts `approvals` and `updates` channels, and other named channels, so config you write now keeps working. Nothing posts to them yet. Milestone 3 brings approval buttons in the `approvals` channel and progress in `updates`.

## Conversations and threads

- **Each new message starts a conversation.** In direct messages and in a channel alike, a message that isn't in a thread is a new conversation, with an engine session of its own.
- **Reply in the thread to carry on.** Wheems answers in your message's thread, and every reply in that thread goes to the same session, with everything said so far. A new top-level message starts afresh.
- **👀 means it has reached the agent.** It goes when the agent answers.
- **Answers come from the engine's own record**, not the screen. The agent's last message of each turn is posted as its answer. Long answers are split into several posts.
- **Progress notes.** For long work, the agent posts a short note first with `wheems reply "…"`.
- **Messages while it's working** are passed on when it finishes its turn.
- **Notices from Wheems itself** start with *Wheems:*, such as when the agent is waiting for an answer at the Mac.
- **Each conversation is a Herdr session.** Watch it, or take over, with the command Wheems gives in its notices, such as `hc attach t7`. `hc ls` lists them; their names start with the workspace id.
- **At most four sessions per workspace** run at once. The least recently used idle one is stopped to make room, and its conversation resumes where it left off when you next reply in its thread.
- **Restarts don't lose threads.** Which thread belongs to which session is kept in `~/wheems/.wheems/conversations.json`, so a reply after a restart, or days later, resumes the same conversation.
- **Quiet conversations write a note.** After 30 minutes without new messages, Wheems asks the agent for a note for the knowledge base, as a turn of its own. That turn's answer is never posted to Slack. See [Knowledge base and search](https://wheems.dev/docs/knowledge-base/).

> **Files aren't read yet:** A message with a file or image attached is ignored. Send text, or put the file in the workspace folder and say where it is.

## One Slack app for every workspace

You create the Slack app once. Every Wheems workspace binds a channel on it: one gets its direct messages, the rest get a channel each. The tokens are saved once, under the connection `slack` in `install.yaml`.

![One Slack app, "Wheems", saved as the connection slack, serves three workspaces: direct messages go to personal (chief of staff on Claude Code), #acme-chat to acme (chief of staff on Claude Code) and #sales-chat to sales (chief of staff on Codex).](https://wheems.dev/docs/diagrams/slack-app.svg)

`wheems setup` and `wheems init` assume this one connection, named `slack`. Connecting a second Slack workspace isn't supported by setup today.

### Identities: how posts appear

Each workspace's posts can appear under its own name and icon, through the `chat:write.customize` scope. `wheems init` sets the name; add an icon if you like:

```yaml title="~/wheems/workspaces/acme/workspace.yaml"
identity: { name: Acme chief of staff, icon: ':office:' }
```

`icon` is an emoji code such as `:house:`, or the URL of an image. Without `identity`, answers appear under the bot's own name. Notices from Wheems (*Wheems: …*) always use the bot's own name.

## Testing it end to end

Work down this list after connecting Slack, or whenever something seems wrong.

1. **Doctor passes.** Look at Channels and Service:

   ```bash title="Terminal"
   wheems doctor
   ```

   ```text title="Output"
   Channels
     ✓ slack: Slack: Signed in as wheems in Acme

   Service
     ✓ service: Running and answering
   ```

2. **The service connected and bound your channels.**

   ```bash title="Terminal"
   wheems service logs -n 20
   ```

   ```text title="Output"
   2026-11-13T09:00:01.118Z Workspace personal: chat on slack (direct messages)
   2026-11-13T09:00:01.402Z Workspace acme: chat on slack C0123ABCD
   2026-11-13T09:00:02.007Z Connected slack
   2026-11-13T09:00:02.031Z Wheems 0.0.3 is running from /Users/you/wheems
   ```

3. **A message gets 👀.** Send "hello" to the bot or in the channel. No 👀 within a few seconds means the message never reached the agent: see the table below.

4. **An answer arrives in the thread**, under the workspace's identity.

5. **A reply carries on.** Reply in the thread: "what did I just say?". The agent remembers.

6. **You can watch it.** Run `hc ls` and find the session named after the workspace, then `hc attach` it. Press Ctrl+\ to detach without stopping it.

## Troubleshooting

Start with `wheems doctor` and `wheems service logs`. Most problems show up in one of them.

| Symptom or message | Cause | Fix |
| --- | --- | --- |
| `Slack refused the bot token: invalid_auth` | The bot token is wrong, from another app, or was revoked. | Copy the **Bot User OAuth Token** again from **OAuth & Permissions**. It starts with `xoxb-`, not `xoxp-`. |
| `Slack refused the app-level token: … It needs the connections:write scope.` | The app-level token is missing `connections:write`, or was revoked. | Generate a new app-level token with `connections:write` under **Basic Information**. |
| `That starts with xapp-, so it's the app-level token. …` (or `xoxb-`, `so it's the bot token`) | The tokens were pasted the wrong way round. | The bot token is under **OAuth & Permissions**, the app-level token under **Basic Information** → **App-Level Tokens**. Setup asks for the bot token first. |
| `That starts with xoxp-, so it's a user token, not the bot token. …` | A user token (or a refresh token, `xoxe-`) was pasted. | Copy the **Bot User OAuth Token** from **OAuth & Permissions**. |
| `That looks like more than one paste.` / `There's something before the xoxb- at the start.` | The field keeps what's in it after a failed check, so pasting again adds to the end. | Clear the field with Backspace, then paste the token once. |
| `A member ID starts with U or W, then letters and digits` | A name, email or channel id was given. | Use **Copy member ID** from the profile. |
| `Slack has no member U012ABCDEF` | The ID is from another Slack workspace, or mistyped. | Copy it again in the Slack workspace the app is installed in. |
| Doctor: `slack: Not connected yet: no credentials saved for it` | Slack was never connected, or `WHEEMS_SECRETS` points at another file. | Run `wheems setup` → **Channels** → **Slack**. |
| Doctor: `slack allowed users: Nobody is allowed to talk to agents yet` | `allowed_users` is empty. | Run `wheems setup`, or add your member ID to `allowed_users` and restart the service. |
| Doctor: `service: Not running, so nothing answers in chat` | The service isn't installed. | `wheems service install` |
| No 👀, and the log says `Ignored a message on slack from U…, who is not in allowed_users` | That person isn't allowed. | Add their member ID to `allowed_users`, then `wheems service restart`. |
| No 👀 in a channel; the log says `the bot cannot see channel …` | The channel name is wrong, or it's a private channel the bot isn't in. | Fix `channels.chat`, or `/invite @Wheems`, then `wheems service restart`. |
| No 👀 in a public channel; the log says `chat on slack C…` | The bot can see the channel but isn't a member, so Slack sends it nothing. | `/invite @Wheems` in the channel. |
| No 👀 after inviting the bot or editing config | The service reads channels and config only when it starts. | `wheems service restart` |
| No 👀 on a message with a file | Messages with files attached are ignored today. | Send text only. |
| No 👀 in direct messages, but channels work | Another workspace has `direct: true`, or none has. | Check `channels.chat` in each `workspace.yaml`. Only one may have `direct: true`. |
| **Sending messages to this app has been turned off** in the Messages tab | The app was created without `messages_tab_read_only_enabled: false`, or Slack hasn't caught up. | Check **App Home → Messages Tab** allows messages, then quit and reopen Slack. |
| *Wheems: The agent hasn't started on this yet. It may be waiting for an answer at the Mac: `hc attach t7`.* | The engine is showing a question, usually whether to trust the workspace folder. | Run the `hc attach` command, answer it, and detach with Ctrl+\. `wheems doctor` checks folder trust. |
| *Wheems: The agent is waiting for an answer at the Mac, such as a permission prompt* | The agent runs with `permissions: ask`, or met something risky on `auto`. | Answer it with `hc attach`. For a chief of staff you use from your phone, use `permissions: auto`. |
| *Wheems: The agent finished without replying here.* | The turn ended with no answer Wheems could read. | Attach to the session to see what happened, then reply in the thread. |
| *Wheems: Something went wrong: …* | An error while handling your message. | Read the rest of the notice and the service log. |
| Answers appear under the bot's name, not the workspace's | No `identity` in `workspace.yaml`, or the app lacks `chat:write.customize`. | Add `identity`, and recreate or update the app from the current manifest. |
| Messages reach Wheems only some of the time | Two services are connected with the same app-level token, such as on two Macs. Slack shares events between open connections. | Run one service per Slack app: `wheems service uninstall` on the other Mac. |

## Slack's free plan

Wheems works on Slack's free plan:

- **Apps:** a free Slack workspace can add up to 10 apps and integrations. Wheems uses one, however many Wheems workspaces you have.
- **History:** a free plan shows the last 90 days of messages. Older threads disappear from view. What matters is kept on your Mac: conversations in `~/wheems/.wheems/conversations.json` and what was learned in each workspace's knowledge base.
- **Channels:** each workspace after the first needs a channel. Projects with their own lead and channel (milestone 4) will need more.

## Next

- [Quick start](https://wheems.dev/docs/quick-start/): the rest of setup, from nothing to a first reply.
- [Running as a service](https://wheems.dev/docs/service/): logs, restarts and launchd.
- [Configuration reference](https://wheems.dev/docs/reference/configuration/): every key in `install.yaml` and `workspace.yaml`.
