Claude Code ⇄ Slack
This server gives Claude Code a set of tools to talk to Slack, post and manage messages, share files, and monitor whether the bridge is working.
Send messages to a channel or reply inside a thread with
send_slack_messageRewrite or update an already-posted message in place with
update_slack_messageUpload local images/files into a channel or thread with
send_slack_imageCreate live, self-updating checklists for multi-step jobs with
slack_progressCreate durable reference material as a Slack canvas with
create_slack_canvasCheck the end-to-end health of the bridge, watcher status, and pending messages with
slack_statusRead unread @mentions from the per-channel inbox with
check_slack_inbox
Runs a Slack app in Socket Mode to bridge a Slack workspace with Claude Code sessions. Posts live turn cards that update with elapsed time, tool count, and tokens; delivers replies in threads; converts Markdown to Slack's dialect; splits long messages safely; handles direct messages and file uploads; maintains a per-channel presence line (connected/offline) pinned to the bottom of the channel; and provides tools like slack_progress for checklists and slack_status for health checks.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Claude Code ⇄ Slack ask Claude in #dev to fix the login redirect bug and report back"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Claude Code ⇄ Slack
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 repliesThe 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 repliesClaude'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
⏳ verifyRelated MCP server: Channel Bridge MCP
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 syntaxLong messages are split on line boundaries, never truncated and never cut inside a code fence
Direct messages work without an
@mentionImages 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 messageA 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
Mentions sent while nothing was connected are recovered on the next start — Socket Mode drops events for a disconnected app, so these used to be lost outright rather than merely late
The watcher is supervised — if it crashes it is respawned, rather than taking inbound down for the rest of the session
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)
thinkingblocks have no path to the outputCredentials are masked on the way out:
pass=, Slack tokens, connection strings, bearer headers, private keys, long hex runsTool 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_TOKENand 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 |
| Posting, editing and deleting the bot's own messages |
| Reading messages in public channels |
| 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 |
| No 👀 / ✅ marks on your message. Everything else works. |
| The bot cannot be used in a DM (see step 5) |
| Images and files people send cannot be downloaded |
|
|
|
|
| Only needed if you set |
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 |
| Public channels |
| Private channels |
| 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
403with 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 @YourBotNameWithout 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 8The 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:
git clone https://github.com/Palgenius/claude-code-slack-bridge.git
cd claude-code-slack-bridge && npm install3. Add .mcp.json to your project:
{
"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.
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
One page — the session ritual, symptom→cause table, every tool and command. Start here. | |
Full procedure for wiring a project, and troubleshooting in the order that finds the problem fastest | |
Why the design is what it is — the capability gate, Socket Mode load-balancing, the transcript format | |
Every change and the reasoning behind it, including the failures |
Tools Claude gets
Tool | For |
| Is the bridge actually working, end to end |
| Replying. Pass |
| Rewriting a message in place |
| A checklist for a multi-step job, kept to one message |
| A screenshot or chart, inline |
| Reference material that shouldn't scroll away |
| Reading unread mentions manually |
Architecture
Slack ──socket──► webhook.ts ──► slack-inbox-<channel>.jsonl
│ │
│ ▼
│ supervise-watch.mjs → watch-mentions.mjs
│ (Monitor, persistent)
│ │
│ ▼
│ Claude Code session
│ │
└────◄── tools ◄───────┘
~/.claude/projects/…/<session>.jsonl
│
▼ tailed, when SLACK_STREAM=1
the live turn cardModule | |
| MCP server: tools, Slack listener, the live view |
| Per-channel mention inbox, mention matching, subtype rules |
| Post, edit, upload, canvas, react — |
| Markdown → Slack mrkdwn, and splitting an over-long message |
| Reading turns out of the transcript, and rendering the live card |
| The |
| The 🟢/⚪ status line and the heartbeat files |
| Finding and tailing the session transcript, and redaction |
| Turns a stored mention into an event in the session |
| Keeps the watcher alive — what a session actually starts |
Tests
npm test281 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.mdsection makes Claude start it; that is advice to a model, not a hook. Verify withslack_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_imagewill 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
by Dhaval Nagar — 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.
Available Tools
7 toolscheck_slack_inboxA
Read Slack messages that @mentioned the bot and have not been read yet. Use this when the session cannot receive channel notifications (started without --channels), or to catch up on anything missed. Each message comes back with its channel id, so you can reply with send_slack_message.
Inputs: peek.
create_slack_canvasA
Create a Slack canvas in a channel from markdown. Use for reference material people will come back to — a checklist, a runbook, a spec — rather than a message that scrolls away. Supports headings, lists, checkboxes, bold and code.
Inputs: title (required), channel (required), markdown (required).
send_slack_imageA
Upload a local image or file into a Slack channel, so it appears inline rather than as a link. Use for screenshots, diagrams and charts. The path must be a file on this machine.
Inputs: channel (required), file_path (required), title, comment, +1 more.
send_slack_messageA
Send a message to a Slack channel. Use this to reply to webhook channel incoming messages from Slack. Returns the message timestamp (ts) — pass it to update_slack_message to rewrite this message later, which is how a progress or "working on it" marker is kept to one line instead of many.
Inputs: text (required), channel (required), thread_ts.
slack_progressA
A live checklist in Slack, kept to one message that rewrites itself as the work moves. Call once with steps to post the board and get back a ts; call again with that ts and update to change a step. Use it for any job with several stages — it replaces a run of "doing X now" messages with one line per step that people can glance at. Statuses: pending, active (the one running now), done, failed, skipped.
Inputs: channel (required), ts, steps, title, +4 more.
slack_statusA
Check whether the Slack bridge is actually working end to end: is this server connected, is the mention watcher running (without it nothing inbound reaches this session), how many messages are waiting, and what other projects are connected to the same Slack app. Call this first whenever Slack seems unresponsive, or after starting a session, instead of guessing.
Inputs: no named parameters.
update_slack_messageA
Rewrite a message already posted, given its timestamp from send_slack_message. Use it to keep a live status marker on one line: post "working on X", then update it as the work moves and once more when it is done. Far better than posting a new message per step. Only messages this bot posted can be rewritten.
Inputs: ts (required), text (required), channel (required).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
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.
Maintenance
Related MCP Connectors
Hosted Slack MCP server for Claude, ChatGPT and Cursor, with each action switched per account.
Your AI apps as one team: hand work to Codex or Claude Code on your Mac, with a shared logbook.
Work on your own computers from Claude or ChatGPT: run commands, edit and search files.
Chat with your AI agents from Slack: each gets a thread in your DM, and your replies there wake it.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceWraps Claude Code as a server using the Model Context Protocol (MCP), allowing teams to interact with Claude Code through a Slack bot interface.11 npm11MIT
- AlicenseNot gradedqualityDmaintenanceAn independent messaging-channel bridge for Claude Code, with Slack Socket Mode adapter; enables Claude to receive and respond to messages via Slack.MIT
- AlicenseAqualityCmaintenanceSelf-hosted coding agent that runs Claude Code or Codex in your own infrastructure and works from Slack. Exposes codebase investigation, file edits, branch and PR creation, allowlisted command execution, CI status and an audit log as MCP tools, keeping every credential behind the tool layer instead of handing it to the model.1212 npm2MIT
- AlicenseNot gradedqualityCmaintenanceRelays Slack direct messages into a locally running Claude Code session over MCP, so users can talk to their local agent from Slack. Replies, reactions, and execution-permission approvals (buttons or yes/no replies) are all handled inside Slack without opening ports or tunnels.41 npmMIT