@imran-ansari/slack-claude-bridge
v0.1.1
Published
Self-hosted two-way bridge between Slack and Claude Code. Socket Mode receiver, runs claude -p in a configured worktree, mirrors Conductor sessions to Slack threads, supports reply-to-resume.
Downloads
40
Maintainers
Readme
slack-claude-bridge
ok so basically — this is a self-hosted lil bridge between Slack and Claude Code. runs on your machine, talks to Slack over Socket Mode, shells out to the claude CLI for you. tl;dr: drive claude on your laptop from anywhere your phone is. no cap.
what you'd actually use this for
run claude from your phone like an absolute menace. POV: you're at lunch, sandwich in one hand, designer slacks you "the upload icon is doing the most rn". you do not put the sandwich down:
you:
@bot make the upload icon 20% larger and add a soft hover statebot: 👀 → (2 min later) drops a before/after screenshot
three minutes, no laptop, designer's gagged, sandwich still warm. main character energy fr.
resume a hot session from your couch like nothing happened. mid-feature you gotta dip — meeting, dinner, gym, life. that session was cooked: full context loaded, dev server warm, auth state cached. losing it would be in shambles behavior. with the mirror enabled (peep the section below), the last turn auto-lands in a Slack thread. couch-locked on your phone, you reply:
you: keep going, but make the empty state match the design tokens
claude picks up exactly where it left off — same session ID, same cwd, same loaded context. zero re-priming, zero "wait what was i doing". it's giving "remote desktop, but for your brain". iykyk.
5 worktrees in parallel, one channel, zero alt-tab psychic damage. running Claude across a bunch of Conductor worktrees? bestie, each enrolled workspace gets its own Slack thread when a turn ends. one channel, you scroll, you see what's cooking, what's stuck, what's begging for input. cycling through 5 terminal tabs is so 2023 — this is just a feed now. the way it scales is criminal.
show stakeholders without handing them the keys to the repo. PM wants a peek at the new dashboard. you'd rather chew glass than record a Loom or grant repo access:
PM (in Slack):
@bot screenshot the dashboard with the empty statebot: (uploads 3 PNGs in the thread)
they iterate in the thread, you stay heads-down on the real work. PR review without a PR. they think you have 4 hands. let them cook with that assumption.
ship a fix mid-call without anyone clocking it. bug drops in standup. instead of "i got it after this" energy (and then forgetting, respectfully), you slip a @bot patch the null-check on the user avatar fallback into Slack while someone else is talking. meeting ends, the fix is sitting in your DMs as a screenshot. you commit it for real once you're back at the keys. multitasking? we're inventing it. the math is mathing.
how it works (the two modes)
- one-shot mentions —
@bot do thing→ runs theslack-respondskill in your worktree → posts back a screenshot or short clip. ate. - conductor session mirror — drop a
Stophook into your Claude Code config and every turn from any opted-in workspace gets mirrored into a Slack thread. reply in the thread → it feeds right back into the same session viaclaude -p --resume. one channel, every workspace, no notes.
@bot "make the upload icon larger" (one-shot path, single worktree)
│
▼
Slack Socket Mode ──► slack-claude-bridge ──► claude -p in apps[].cwd
[any Conductor workspace turn ends] (mirror path, all workspaces)
│
▼
.slack-mirror present? ─yes─► Stop hook ──► POST 127.0.0.1:8765/notify
│
▼
chat.postMessage in mirror.channel
(thread keyed by session_id)
[you reply in any mirror thread]
│
▼
slack-claude-bridge ──► claude -p --resume <session-id> "<reply>" in session.cwd
│
▼
Stop hook fires again → posts back in same threadwhy this exists
- one-shot path scales by
apps[]: one Slack app per worktree, or just one shared app if you're chill. - mirror path scales horizontally: one channel, one app, every workspace on your machine. just
touch .slack-mirrorin whatever worktree you wanna follow remotely. lowkey magic.
plain Node + TypeScript + Slack Socket Mode (outbound WebSocket — nothing inbound, breathe easy). notify server binds 127.0.0.1 only — your laptop, your rules. Python 3 for the hook script.
what you need
- Node ≥ 20
claudeCLI onPATHffmpegonPATH(only if you want clip mode, otherwise skip)- Python 3 on
PATH - a Slack workspace where you can spin up apps
quick start (the tl;dr)
npm install -g @imran-ansari/slack-claude-bridge
# pick a directory to live in (config + sessions land here)
mkdir -p ~/.slack-claude-bridge && cd ~/.slack-claude-bridge
# drop a starter config
slack-claude-bridge init
# fill it in — see the field guide below
$EDITOR config.json
# go
slack-claude-bridgethen in Slack: @bot ping. bot reacts 👀, runs claude, posts back. you're cooking.
want the mirror feature? scroll down to conductor session mirror.
heads up:
slack-claude-bridgereads./config.jsonfrom whatever directory you launch it in. pick a directory and stick with it — that's also wheresessions.json(mirror state) and thecaptures/folder land.
CLI
slack-claude-bridge start the server (reads ./config.json)
slack-claude-bridge start same as above
slack-claude-bridge init copy the starter config into the cwd
slack-claude-bridge hook-path print the absolute path to the Stop-hook script
slack-claude-bridge --help show helpenv: CONFIG_PATH=/path/to/config.json slack-claude-bridge to read a config from elsewhere.
slack app setup (the necessary evil)
do this once per worktree if you want isolated one-shot apps, or just once if you're cool with one shared app handling everything.
- https://api.slack.com/apps → Create New App → From scratch.
- name it, pick the workspace, flip Socket Mode ON.
- Basic Information → App-Level Tokens → Generate Token. add
connections:write. stash thexapp-...somewhere safe. - OAuth & Permissions → Bot Token Scopes. add these:
app_mentions:read,chat:write,files:write,reactions:writechannels:history,groups:history,im:history,mpim:history
- Event Subscriptions: ON. subscribe to:
app_mention,message.channels,message.groups,message.im,message.mpim
- Install to Workspace, grab the
xoxb-...and the App ID (A0XXXXX). - spin up a (private) channel,
/invitethe bot, copy that channel ID.
bet.
config (the part where you fill in the blanks)
slack-claude-bridge init drops a starter config.json in your cwd. then make it look like this:
{
"team_id": "T0XXXXXXXXX",
"learn_mode": false,
"allowed_users": ["U0YOURUSERID"],
"allowed_channels": ["C0YOURCHANNEL"],
"claude_bin": "claude",
"claude_timeout_ms": 0,
"notify_port": 8765,
"sessions_path": "./sessions.json",
"mirror": {
"app_id": "A0XXXXXXXX",
"channel": "C0YOURCHANNEL"
},
"apps": {
"A0XXXXXXXX": {
"label": "my-workspace",
"app_token": "xapp-...",
"bot_token": "xoxb-...",
"cwd": "/absolute/path/to/your/worktree",
"branch": "feature-branch-name",
"dev_url": "http://localhost:3000",
"dev_login": {
"email": "[email protected]",
"password": "...",
"sign_in_path": "/sign-in"
}
}
}
}| field | what it does |
| --- | --- |
| team_id | your Slack workspace ID. events from anywhere else get yeeted. |
| learn_mode | true = log unknown user/channel IDs without rejecting them, so you can grab the IDs you need. flip back to false once you've got 'em. |
| allowed_users / allowed_channels | the allowlist. empty array = wide open (only do this in a private channel, deadass). |
| claude_bin | path to the claude CLI. usually just "claude". |
| claude_timeout_ms | total wall-clock cap. 0 = no cap (the per-ask idle timeout still applies, don't worry). |
| notify_port | localhost port the Stop hook talks to. 0 disables the mirror server entirely. |
| sessions_path | where the session ↔ thread registry gets saved. set and forget. |
| mirror.app_id / mirror.channel | which app posts mirror messages and where they land. independent of apps[].cwd, so the Stop hook works across every workspace. nuke the mirror block to turn mirroring off. |
| apps[].cwd | only used by the one-shot @bot path. the directory claude -p runs in. |
| apps[].dev_url / apps[].dev_login | optional. passed as env vars (DEV_URL, DEV_LOGIN_*) for slack-respond when it grabs screenshots. |
your tokens stay your tokens — keep config.json out of git.
running it as a service
slack-claude-bridge stays in the foreground. for 24/7 vibes:
- macOS —
launchdplist that runsslack-claude-bridgewithWorkingDirectoryset to your config dir. - Linux —
systemduser unit, same vibe. - quick & dirty —
nohup slack-claude-bridge > bridge.log 2>&1 &. it's giving "i'll do it properly later" energy, but it works.
conductor session mirror — setup
install the Stop hook globally in your Claude Code config so it fires every turn. the marker file (.slack-mirror) gates per-workspace — without it the hook just chills and does nothing. safe to install everywhere, no spam.
1. install the hook (one time, you're done forever)
first, find the hook script:
slack-claude-bridge hook-pathit'll print something like /usr/local/lib/node_modules/@imran-ansari/slack-claude-bridge/scripts/stop-hook.py. copy that.
then in ~/.claude/settings.json, append:
{
"hooks": {
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "python3 /paste/the/path/from/hook-path/here"
}
]
}
]
}
}on bash/zsh you can lock this in with one line if you'd rather not paste:
python3 $(slack-claude-bridge hook-path). if you go that route, make sureslack-claude-bridgeis on thePATHof whatever shell Claude Code spawns the hook in.
2. enroll a workspace
cd /path/to/any/workspace
touch .slack-mirror # default label = basename(cwd)
# or, with a custom label:
echo "auth-rewrite-v3" > .slack-mirrornext turn that ends → hook fires → bridge spins up a Slack thread keyed by session_id → every turn after that lands in the same thread. on god.
3. reply from Slack
reply (or @bot reply) in the thread. the bridge runs:
claude -p --resume <session-id> --dangerously-skip-permissions "<your reply>"in that session's recorded cwd. Stop hook fires again → posts the response back in the same thread. infinite loop, but the good kind.
4. when you're over it
- per workspace —
rm .slack-mirror. future turns stop notifying. the thread mapping sticks around insessions.jsonso existing threads still resume on reply (delete the entry if you want a clean break). - globally — yank the Stop hook out of
~/.claude/settings.json.
multi-workspace behavior
the hook ships every mirrored workspace's turns to the same channel set in mirror.channel. threads tell them apart: each parent message shows the label + cwd, you reply in the one you care about. sessions are independent — running 5 mirrored workspaces in parallel just makes 5 sibling threads in that channel. the channel doesn't get confused. you might. that's on you.
ask-back protocol (one-shot)
the slack-respond skill (one-shot path) can pause mid-run and ask you a question — when it needs a decision, a credential, whatever. file IPC under the hood: skill writes captures/ask-<id>.json, bridge posts the question, you reply, bridge writes captures/answer-<id>.txt. it'll wait up to 60 min per ask. patient bot.
developing from source
if you'd rather hack on it directly:
git clone https://github.com/imran1721/slack-claude-bridge.git
cd slack-claude-bridge
npm install
cp config.example.json config.json
# edit config.json
npm run dev # auto-reload via tsx
# or
npm run build && npm startarchitecture:
src/
├── types.ts zod schemas: config, MirrorConfig, ClaudeResult, AskMessage,
│ NotifyMessage, SessionThread
├── util.ts chunkText helper
├── config.ts loadConfig() + getApp()
├── server.ts Socket Mode + notify server boot + event routing:
│ pending ask → mirrored session → new one-shot run
├── dispatch.ts one-shot orchestration
├── runner.ts one-shot claude subprocess + ask-file watcher
├── notify.ts HTTP /notify endpoint (mirror inbound)
├── sessions.ts persistent session ↔ thread registry
├── resume.ts fire-and-forget claude -p --resume spawn (mirror outbound)
├── asks.ts pending-ask registry
└── slack.ts WebClient helpers
bin/
└── cli.js CLI dispatcher: start / init / hook-path / --help
scripts/
└── stop-hook.py Claude Code Stop hook (gated by .slack-mirror marker)security real talk
- tokens live in
config.json. don't commit your filled-in config. like, ever. - the notify server binds
127.0.0.1only — nothing on your network can reach it. the Stop hook posts tolocalhost:<notify_port>and that's it. allowed_users/allowed_channelsare checked before anyclaudeinvocation. only fliplearn_mode: truefor like 5 minutes to find IDs, then flip it back. don't sleep on this.- mirror replies run
claude -p --resume --dangerously-skip-permissions. translation: treat the mirror channel like a shell on your laptop. only invite people you'd hand the keyboard to. fr fr.
troubleshooting (when stuff ain't working)
- mirror not posting → Stop hook isn't reaching the bridge. check, in order:
python3 $(slack-claude-bridge hook-path) < /dev/nullexits 0 cleanly..slack-mirrorexists in the workspace root (or any ancestor folder).curl -s -o/dev/null -w "%{http_code}" http://127.0.0.1:8765/returns404(means server's up — only/notifyis registered, so 404 is the W).mirror.app_id+mirror.channelare set inconfig.json.
- replies in mirror thread don't trigger a resume → bridge log should show
[resume sid=...]lines on reply. if they're missing, you forgotmessage.*events or the*:historyscopes, or you didn't reinstall the app after adding them. invalid_auth→ bot token rotated on reinstall. refreshxoxb-...inconfig.json. classic.channel_not_foundon mirror posts → the bot isn't inmirror.channel./invite @your-botfrom that channel and you're good.- hook fires but nothing happens → bridge isn't running, or
notify_portinconfig.jsondoesn't matchSLACK_BRIDGE_NOTIFY_PORT(defaults to8765). set the env var on the hook command if you changed the port. server build missingwhen you runslack-claude-bridgefrom a clone → runnpm run buildfirst. the global install ships pre-built; the source clone doesn't.
license
MIT — see LICENSE.
