A one-way MCP channel that forwards Slack events (messages and reactions) into a Claude Code session, enabling Claude to react to Slack activity without leaving the terminal.
SaferSkills independently audited claude-slack-event-channel-mcp (MCP Server) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
A one-way (listen-only) Claude Code channel that forwards Slack events into a Claude Code session. It connects to Slack via Socket Mode, filters messages and reactions, and pushes them to Claude as <channel> notifications so Claude can react to things happening in Slack without leaving the terminal.
A channel is an MCP server that Claude Code spawns as a subprocess and talks to over stdio. Instead of waiting for the user to type, the channel pushes events into the session. One-way channels (like this one) forward alerts, webhooks, or chat messages for Claude to act on; two-way channels also expose a reply tool so Claude can send messages back. This server is one-way: Slack → Claude only.
The channel contract is just three things, all implemented in main.ts:
experimental: { 'claude/channel': {} } key in theMcpServer constructor is what registers the notification listener and makes this an MCP server a channel.
mcp.server.notification({ method: 'notifications/claude/channel', params: { content, meta } }).
mcp.connect(new StdioServerTransport()); Claude Code owns the process.The instructions string in the constructor is added to Claude's system prompt so Claude knows what these events mean and that the channel is one-way.
When a Slack event passes the filters, content becomes the body of a <channel> tag and each meta key becomes a tag attribute (the source attribute is filled in automatically from the server's configured name, slack). For example a thread reply arrives in Claude's context as:
<channel source="slack" kind="message" channel="C0123ABC" user="U0456DEF" ts="1718000000.000100" thread_ts="1718000000.000001">
deploy looks stuck, can you check?
</channel>kind="reaction_added" / kind="reaction_removed" events carry a reaction emoji name and the target ts instead.
Note on `meta` keys: keys must be identifiers (letters, digits, underscores only). Keys with hyphens or other characters are silently dropped, which is why this server usesthread_ts, notthread-ts.
Delivery is fire-and-forget: notifications are not acknowledged. If the session hasn't loaded this server as a channel, or org policy blocks it, events are dropped silently with no error. Events that arrive while Claude is busy are queued and delivered together on the next turn.
On Team/Enterprise plans an admin must enable channels first.
xapp-...) carrying theconnections:write scope. Subscribe the app to the events you want forwarded (message.channels, reaction_added, reaction_removed, …) and invite it to the target channels.
bun installCopy .env.example to .env and fill in the values (loaded via dotenv):
| Variable | Required | Description |
|---|---|---|
SLACK_APP_TOKEN | yes | App-Level Token (xapp-...), needs connections:write. |
SLACK_WATCH_CHANNELS | no | Comma-separated channel IDs to forward. Empty = all channels. |
SLACK_ALLOWED_SENDERS | no | Comma-separated user IDs / bot_ids allowed. Empty = allow all. |
Security — this is a prompt injection vector. Every forwarded message becomes Claude's input. Anyone who can post in a watched channel can put text in front of Claude. LeavingSLACK_ALLOWED_SENDERSempty is only safe for channels that solely trust an alert bot; always set an allowlist when listening to human messages. The server gates on the sender's identity (user/bot_id), not the channel, so membership in a watched channel alone is not enough.
Claude Code launches the server itself — you do not run it standalone. Add it to your MCP config so Claude Code spawns it as a subprocess. Project-level .mcp.json (relative path):
{
"mcpServers": {
"slack": { "command": "bun", "args": ["./main.ts"] }
}
}For user-level config in ~/.claude.json, use the absolute path to main.ts so it resolves from any project.
Custom channels aren't on the approved allowlist yet, so start Claude Code with the development flag (this bypasses the allowlist for this one entry after a confirmation prompt; org policy still applies):
claude --dangerously-load-development-channels server:slackThe first time, Claude Code asks for consent to use the new server from .mcp.json — select Use this MCP server. A dim notice under the startup banner confirms the channel is registered. Once running, posting in a watched Slack channel pushes the event straight into the session.
If events don't arrive, run /mcp in the session to check the server's status; "Failed to connect" usually means an import/dependency error — check ~/.claude/debug/<session-id>.txt.
You can run the server directly to verify it connects to Slack (it will throw if SLACK_APP_TOKEN is missing). Note that outside Claude Code there's no channel listener, so notifications go nowhere — this is only useful for checking the Socket Mode connection and your filters.
bun run start # or: bun run dev (watch mode)| Script | Description |
|---|---|
bun run start | Run the server. |
bun run dev | Run with file watching. |
bun run check | Biome lint + format (auto-fix). |
bun run lint | Biome lint only. |
bun run format | Biome format (write). |
bun run typecheck | TypeScript type check. |
(Telegram, Discord, iMessage, fakechat) — including two-way reply tools and sender-pairing flows.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.