Claude Code β Slack
# Claude Code β Slack
[](https://github.com/Palgenius/claude-code-slack-bridge/actions/workflows/test.yml)
[](LICENSE)
[](package.json)
Talk to Claude Code from a Slack channel, and watch it work while it answers.
An MCP server that runs a Slack app in Socket Mode β no tunnel, no public IP,
nothing leaves your machine except what you send to Slack.
## What it looks like in Slack
You `@mention` the bot in a channel. Your message gets π the moment Claude
picks it up, and β
when it finishes β so a question that was never seen is
obvious without reading anything.
```
Ali 10:14
@claude why is the login page redirecting to itself?
π
claude α΄α΄α΄ 10:14
ⳠWorking⦠· 1m 12s · 14 tools · 7.8k tokens
why is the login page redirecting to itself?
β¦3 earlier
Bash Run the full test suite
Read auth.ts
Edit routes.ts
β 2 replies
```
The card is **one message that rewrites itself** β the clock ticks, tool calls
scroll past. When the turn ends it becomes a summary of what changed:
```
claude α΄α΄α΄ 10:16
β
Done Β· 2m 40s Β· 23 tools Β· 31k tokens
why is the login page redirecting to itself?
touched auth.ts routes.ts
β 2 replies
```
Claude's actual answer arrives **in the thread**, under your question:
```
β claude α΄α΄α΄
The guard tested `req.path`, which Express strips to the
router-relative path, so its exemption never matched. Fixed in
routes.ts β only new accounts had the flag, which is why only
new users hit it.
```
A quick answer posts **no card at all**. The card appears once a turn calls a
tool or runs past ten seconds; below that the reply in the thread is the whole
record, and the channel stays quiet.
### The channel always says whether anyone is home
```
claude α΄α΄α΄ 10:02
π’ Claude is connected Β· my-project
Listening here since 10:02 β @mention me and I'll pick it up.
```
And if the server is up but nothing is delivering messages to the session, it
says that instead β because "connected" would be a lie of omission:
```
claude α΄α΄α΄ 10:04
π‘ Claude is connected but not listening Β· my-project
Nothing is delivering messages to the session, so anything written
here will wait unread. The session needs to start its mention watcher.
```
**That is the whole reason you never have to ask whether it is working.** The
one failure that looks like silence announces itself in the channel.
And if you @mention while it is in that state, you get an answer rather than
nothing:
```
β claude α΄α΄α΄
π‘ Got this, but nothing is delivering it to the session yet β
so it is saved and unread rather than answered.
```
Worth knowing why this happens at all: **Claude Code does nothing at session
open.** It acts only when prompted, so the `CLAUDE.md` instruction that starts
the watcher runs on the session's first message rather than when the session
opens. Open a session, go straight to Slack, and nothing is listening yet.
Exactly one of these per channel, moved to the bottom whenever it changes β so
it is never buried under a day of conversation. When the session ends, the same
line becomes:
```
βͺ Claude is offline Β· my-project
Was connected 10:02β18:30 (8h 28m). Nothing is listening in this
channel right now.
```
Without it, a quiet channel and a dead one look identical.
### A checklist for longer jobs
`slack_progress` keeps a multi-step job to one self-rewriting message:
```
claude α΄α΄α΄ 11:31
Deploy
β
read the deployed file
β
upload the release
β³ run the deploy script β bold: the one running now
β³ verify
```
---
## Why this exists
Claude Code delivers `notifications/claude/channel` **only** when the session was
started with `--channels`, a research-preview flag the desktop app does not pass.
The notification is accepted and then dropped by a capability gate β silently,
with no error in any log, in Slack, or on screen.
So the obvious design (MCP server pushes the message at Claude) cannot work on
most machines. This project works around it: mentions are written to a
per-channel inbox on disk, and a small watcher turns each new one into an event
in the session. Outbound has no counterpart API at all, so the live view is built
by tailing the session transcript Claude Code already writes.
Everything here was verified against a live workspace. Where something is a
recommendation rather than a fact about the code, the docs say so.
---
## Features
**Conversation**
- Replies land **in the thread** you asked in, not at the top of the channel
- **Markdown is converted** to Slack's dialect β `**bold**`, headings, links and
fenced code render properly instead of showing their syntax
- Long messages are **split on line boundaries**, never truncated and never cut
inside a code fence
- **Direct messages** work without an `@mention`
- Images and files you send are downloaded so Claude can actually look at them
**Seeing what's happening**
- A **live turn card** that ticks with elapsed time, tool count and tokens, then
collapses to the list of files the turn changed
- **`slack_progress`** β a checklist kept to one self-rewriting message
- A **presence line** per channel: π’ connected / βͺ offline β exactly one message,
moved to the bottom of the channel when it changes so it is never buried
- **`slack_status`** β one call answers "is this actually working?"
**Not losing things**
- Per-channel inbox and read cursor, so several projects don't read each other's
messages
- A message Slack delivers to the wrong project's server is **routed to the right
inbox** rather than silently dropped
- Servers exit with their session instead of orphaning and holding a socket
- Status lines left behind by a crash are corrected by any other live session
**Care with secrets** (streaming is off by default)
- `thinking` blocks have **no path** to the output
- Credentials are masked on the way out: `pass=`, Slack tokens, connection
strings, bearer headers, private keys, long hex runs
- Tool detail, when enabled, uses only fields written to be read β never a
command string, never file contents
---
## Creating the Slack app
Do this once per workspace. It takes about five minutes. Every step is in the
Slack app settings at <https://api.slack.com/apps>.
### 1. Create the app
**Create New App** β **From scratch** β give it a name (this is what people will
`@mention`, e.g. `claude`) β pick your workspace β **Create App**.
### 2. Turn on Socket Mode
Left sidebar β **Socket Mode** β toggle **Enable Socket Mode** on.
It asks for a token name β anything, e.g. `socket`. It then shows a token
starting with **`xapp-`**.
> **Copy it now.** This is your `SLACK_APP_TOKEN` and Slack will not show it
> again. If you lose it, generate a new one under **Basic Information β
> App-Level Tokens**.
### 3. Add the bot scopes
Left sidebar β **OAuth & Permissions** β scroll to **Scopes** β **Bot Token
Scopes** β **Add an OAuth Scope** for each:
**Required** β without these nothing works:
| Scope | Needed for |
| --- | --- |
| `chat:write` | Posting, editing and deleting the bot's own messages |
| `channels:history` | Reading messages in **public** channels |
| `groups:history` | Reading messages in **private** channels |
**Optional** β each one adds a feature, and skipping it costs only that
feature. Nothing breaks; the log says what was refused:
| Scope | Without it |
| --- | --- |
| `reactions:write` | No π / β
marks on your message. Everything else works. |
| `im:history` | The bot cannot be used in a DM (see step 5) |
| `files:read` | Images and files people send cannot be downloaded |
| `files:write` | `send_slack_image` fails |
| `canvases:write` | `create_slack_canvas` fails |
| `pins:write` | Only needed if you set `SLACK_STATUS_PIN=1` |
**Add nothing else.** This token ends up in a config file on your machine, so
every extra scope is something it can do if it leaks. In particular you need no
**User Token Scopes** at all β everything runs on the bot token.
### 4. Subscribe to message events
Left sidebar β **Event Subscriptions** β toggle **Enable Events** on.
There is **no Request URL to fill in** β Socket Mode replaces it. Ignore that box.
Open **Subscribe to bot events** and add:
| Event | For |
| --- | --- |
| `message.channels` | Public channels |
| `message.groups` | Private channels |
| `message.im` | Direct messages |
Then **Save Changes** at the bottom of the page. It is easy to miss.
### 5. Let people DM the bot
Left sidebar β **App Home** β scroll to **Show Tabs** β turn the **Messages
Tab** on, and tick **"Allow users to send Slash commands and messages from the
messages tab."**
> Skip this and the DM box is **read-only** β you cannot even type to the bot.
> The scope and the event from the previous steps are not enough on their own,
> and nothing about the failure points at this setting.
Only needed for DMs. Channels work without it.
### 6. Install it
Left sidebar β **Install App** β **Install to Workspace** β **Allow**.
You now get a **Bot User OAuth Token** starting with **`xoxb-`**. This is your
`SLACK_BOT_TOKEN`.
> β οΈ **Every time you change a scope after this, come back and click
> "Reinstall to Workspace"** (a yellow banner appears at the top). A new scope
> does nothing until you do. The usual symptom is a `403` with an HTML page
> where a file should be.
### 7. Invite the bot to your channel
In Slack itself, in the channel you want to use:
```
/invite @YourBotName
```
Without this the bot receives nothing from that channel, whatever scopes it has.
### 8. Get the channel ID
Right-click the channel in the sidebar β **View channel details** β scroll to
the bottom. The ID starts with **`C`**, e.g. `C01ABCDEFGH`. This is your
`SLACK_CHANNEL_ID`.
*(It is also the last part of the channel's URL in the browser version.)*
### You should now have three values
```
SLACK_APP_TOKEN xapp-β¦ from step 2
SLACK_BOT_TOKEN xoxb-β¦ from step 6
SLACK_CHANNEL_ID C⦠from step 8
```
The two tokens are **not** interchangeable. Swapping them gives
`not_allowed_token_type` on every send, which is a confusing error to debug.
---
## Quick start
**1. Create the Slack app** β the eight steps above.
**2. Install:**
```bash
git clone https://github.com/Palgenius/claude-code-slack-bridge.git
cd claude-code-slack-bridge && npm install
```
**3. Add `.mcp.json` to your project:**
```json
{
"mcpServers": {
"slack-channel": {
"type": "stdio",
"command": "npx",
"args": ["tsx", "/path/to/claude-code-slack-bridge/webhook.ts"],
"env": {
"SLACK_APP_TOKEN": "xapp-...",
"SLACK_BOT_TOKEN": "xoxb-...",
"SLACK_CHANNEL_ID": "C0..."
}
}
}
}
```
**4. Add the watcher section to your project's `CLAUDE.md`** β copy it from
[SETUP-NEW-PROJECT.md](SETUP-NEW-PROJECT.md).
**5. Restart Claude Code**, then ask it: *"is slack connected?"*
---
## β οΈ The one thing to know
There are **two** processes and only one starts by itself.
| | Starts automatically? | Without it |
| --- | --- | --- |
| **MCP server** | β
with the session | Nothing works |
| **Mention watcher** | β **no** | Messages arrive, are stored correctly, and **nothing ever surfaces them** |
That second failure is the one that will cost you an evening: outbound keeps
working, the channel looks alive, and the silence is indistinguishable from
nobody having written to you.
**So `slack_status` is the first thing to run whenever Slack seems quiet.** The
`Watcher:` line is the answer most of the time.
---
## Documentation
| | |
| --- | --- |
| **[QUICK-REFERENCE.md](QUICK-REFERENCE.md)** | One page β the session ritual, symptomβcause table, every tool and command. **Start here.** |
| **[SETUP-NEW-PROJECT.md](SETUP-NEW-PROJECT.md)** | Full procedure for wiring a project, and troubleshooting in the order that finds the problem fastest |
| [SLACK_MCP_INTEGRATION.md](SLACK_MCP_INTEGRATION.md) | Why the design is what it is β the capability gate, Socket Mode load-balancing, the transcript format |
| [CHANGES.md](CHANGES.md) | Every change and the reasoning behind it, including the failures |
---
## Tools Claude gets
| Tool | For |
| --- | --- |
| `slack_status` | Is the bridge actually working, end to end |
| `send_slack_message` | Replying. Pass `thread_ts` back. |
| `update_slack_message` | Rewriting a message in place |
| `slack_progress` | A checklist for a multi-step job, kept to one message |
| `send_slack_image` | A screenshot or chart, inline |
| `create_slack_canvas` | Reference material that shouldn't scroll away |
| `check_slack_inbox` | Reading unread mentions manually |
---
## Architecture
```
Slack ββsocketβββΊ webhook.ts βββΊ slack-inbox-<channel>.jsonl
β β
β βΌ
β watch-mentions.mjs (Monitor, persistent)
β β
β βΌ
β Claude Code session
β β
ββββββββ tools βββββββββ
~/.claude/projects/β¦/<session>.jsonl
β
βΌ tailed, when SLACK_STREAM=1
the live turn card
```
| Module | |
| --- | --- |
| `webhook.ts` | MCP server: tools, Slack listener, the live view |
| `inbox.ts` | Per-channel mention inbox, mention matching, subtype rules |
| `slackRich.ts` | Post, edit, upload, canvas, react β `fetch` injected so it is testable |
| `mrkdwn.ts` | Markdown β Slack mrkdwn, and splitting an over-long message |
| `turn.ts` | Reading turns out of the transcript, and rendering the live card |
| `progress.ts` | The `slack_progress` checklist |
| `presence.ts` | The π’/βͺ status line and the heartbeat files |
| `transcript.ts` | Finding and tailing the session transcript, and redaction |
| `watch-mentions.mjs` | Turns a stored mention into an event in the session |
---
## Tests
```bash
npm test
```
236 tests over eight modules using the Node test runner through `tsx` β no test
dependencies. `slackRich.ts` takes `fetch` and `fs` as injected dependencies, so
the Slack call sequences are checked without a workspace.
The turn tracker is also replayed against a real session transcript, not only
fixtures. That is how the bug where one finished turn swallowed every later one
was found: every fixture happened to begin with a user prompt, so the case that
breaks it could not appear in one.
---
## Known limits
- **The watcher does not start itself.** The `CLAUDE.md` section makes Claude
start it; that is advice to a model, not a hook. Verify with `slack_status`.
- **Two projects on one Slack app share delivery at random.** Socket Mode
load-balances across every connection. Strays are recovered through the
per-channel inbox, so they are late rather than lost β but prefer one Slack app
per project.
- **No way to interrupt Claude from Slack.** Nothing in the protocol carries it.
- **The live view is a second or two behind** and cannot show a partial sentence.
- **`send_slack_image` will upload any path it is given**, with no allowlist.
- **Markdown tables become monospaced blocks.** Slack has no table syntax, so
the columns are padded to line up rather than arriving as stray pipes.
---
## Credits
Built on [AppGambitStudio/Claude-Code-Slack-Channel](https://github.com/AppGambitStudio/Claude-Code-Slack-Channel)
by [Dhaval Nagar](https://github.com/AppGambitStudio) β the original Socket Mode
MCP server and the `send_slack_message` tool. This project extends it with the
inbox, threading, markdown conversion, the live view, progress boards, presence
and status tooling.
MIT, with the original copyright retained. See [LICENSE](LICENSE).
TDQS
Scored across 7 tools
Each tool has a fairly distinct purpose: reading, sending, updating, uploading images, progress checklist, canvas creation, and diagnostics. There is mild overlap between slack_progress and update_slack_message since both rewrite an existing message, but descriptions clearly explain when to use each (progress board vs. single status line).
Conventions are mixed: check_slack_inbox, send_slack_message, update_slack_message, send_slack_image, create_slack_canvas use verb_slack_noun, while slack_progress and slack_status invert to noun-first. Still readable, but not a predictable pattern across the set.
Seven tools map cleanly onto the Slack bridge's needsβinbound read, outbound send/update, file upload, progress, canvas, and healthβwithout redundancy or thinness.
Covers read, reply, edit, upload, structured progress, persistent canvas, and diagnostics, which is strong lifecycle coverage. Missing a few likely operations like reacting to messages, listing channels, or deleting messages, but these are minor gaps agents can work around.