
Most Slack bot tutorials bury you in OAuth flows and ngrok tunnels. You do not need any of it. Socket Mode lets Bolt talk to Slack over an outbound WebSocket. Pair it with the Anthropic SDK and you have a working agent-in-Slack in one file, no exposed port.
Table of contents
Hey guys, Mr. Technology here.
Enable Socket Mode on your Slack app with chat:write, app_mentions:read, im:history scopes. Copy the App-Level Token (xapp-...) and Bot Token (xoxb-...) from Basic Information.
bash pip install slack-bolt anthropic
bash export SLACK_APP_TOKEN=xapp-... export SLACK_BOT_TOKEN=xoxb-... export ANTHROPIC_API_KEY=sk-ant-...
Save as bot.py. Threads map cleanly onto Claude conversation turns.
```python import os, anthropic from slack_bolt import App from slack_bolt.adapter.socket_mode import SocketModeHandler
app = App(token=os.environ["SLACK_BOT_TOKEN"]) client = anthropic.Anthropic() SYSTEM = "You are a concise assistant for the engineering team. " \ "Answer in 2-4 sentences unless asked for code."
def history_to_messages(thread: list[dict]) -> list[dict]: bot_id = app.client.auth_test().data["user_id"] msgs = [] for m in thread: if m.get("bot_id") or m.get("subtype") == "bot_message": continue role = "assistant" if m.get("user") == bot_id else "user" msgs.append({"role": role, "content": m["text"]}) return msgs[-10:]
@app.event("app_mention") def on_mention(event, say, client): thread_ts = event.get("thread_ts") or event["ts"] replies = client.conversations_replies( channel=event["channel"], ts=thread_ts, limit=20 )["messages"] msgs = history_to_messages(replies) resp = client.messages.create( model="claude-sonnet-4-5", max_tokens=512, system=SYSTEM, messages=msgs, ) say(text=resp.content[0].text, thread_ts=thread_ts)
@app.event("message") def on_dm(event, say, client): if event.get("channel_type") != "im": return msgs = history_to_messages([event]) msgs.insert(0, {"role": "user", "content": event["text"]}) resp = client.messages.create( model="claude-sonnet-4-5", max_tokens=512, system=SYSTEM, messages=msgs, ) say(text=resp.content[0].text, channel=event["channel"])
if __name__ == "__main__": SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start() ```
bash python bot.py
DM the bot or @-mention it in a channel. It replies in the thread.
Socket Mode means zero surface area. No public URL, no ingress. Your laptop opens an outbound WebSocket and Slack streams events to it — the right default for internal tools.
Thread = conversation. conversations_replies returns parent plus replies; feed the last N turns to the model. Memory is free. Cap with msgs[-10:] for cost.
Two handlers, one model. app_mention for channels, message filtered to channel_type == "im" for DMs. Do not over-engineer until you have a second use case.
Bot echo loops. Skipping bot_id is mandatory — without it the bot replies to itself and your token bill explodes in four turns.
Rate limits. ~1 msg/sec per channel. For long answers post a placeholder, then chat.update when the model returns. say() returns the timestamp.
Context growth. limit=20 × 4 KB = 80 KB per call. Trim or summarize older turns with a cheaper Haiku pass.
Streaming is awkward. Bolt does not stream edits natively. Post the full response as one block — users prefer one notification over ten partials.
Permissions. Bot only sees channels it has been invited to. New install means /invite @YourBot everywhere.
Add tools=[{"name": "lookup_pr", ...}] to messages.create, dispatch tool_use, return tool_result next turn — you have a Slack-resident agent. Deploy the same file unchanged on Fly.io or EC2.
Bolt Socket Mode is the fastest path from "I have an agent" to "my team is using it." One file, two handlers, zero infra.
— Mr. Technology
*Tested June 2026 with slack-bolt 1.18+ and the anthropic Python SDK on claude-sonnet-4-5. Socket Mode requires a Slack workspace where you can install custom apps — free tier works.*
Sources: