claude-slack-channel-bots 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +328 -0
- package/hooks/ask-relay.sh +50 -0
- package/hooks/permission-relay.sh +78 -0
- package/package.json +43 -0
- package/skills/setup-slack-channel-bots/SKILL.md +387 -0
- package/slack-app-manifest.yml +49 -0
- package/src/cli.ts +201 -0
- package/src/config.ts +222 -0
- package/src/health-check.ts +89 -0
- package/src/lib.ts +275 -0
- package/src/pid.ts +70 -0
- package/src/postinstall.ts +90 -0
- package/src/registry.ts +655 -0
- package/src/restart.ts +177 -0
- package/src/server.ts +1357 -0
- package/src/session-manager.ts +176 -0
- package/src/sessions.ts +77 -0
- package/src/tmux.ts +168 -0
- package/src/tokens.ts +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gabe Mahoney
|
|
4
|
+
Originally based on claude-code-slack-channel by Jeremy Longshore
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# Slack Channel Router
|
|
2
|
+
|
|
3
|
+
A single HTTP MCP server that holds one Slack Socket Mode connection and routes messages to multiple independent Claude Code sessions, each scoped to a different repo and reachable via its own Slack channel. Inbound messages are dispatched to whichever session owns the channel they arrived on; outbound tool calls are restricted to channels that session has previously received a message from.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
1. **Install globally via bun:**
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bun install -g claude-slack-channel-bots
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The postinstall script creates skeleton config files in `~/.claude/channels/slack/`.
|
|
16
|
+
|
|
17
|
+
2. **Run the setup skill:**
|
|
18
|
+
|
|
19
|
+
The package includes a Claude Code skill at `skills/setup-slack-channel-bots/` that walks you through the entire configuration. Copy or symlink it into `~/.claude/skills/`, then run:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
claude /setup-slack-channel-bots
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
It handles Slack app creation, tokens, routing, access control, hooks, and validation — and skips anything already configured.
|
|
26
|
+
|
|
27
|
+
3. **Start the server:**
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
claude-slack-channel-bots start
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
See the sections below for manual configuration details if you prefer not to use the skill.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Prerequisites
|
|
38
|
+
|
|
39
|
+
- [Bun](https://bun.sh) v1.0+
|
|
40
|
+
- [tmux](https://github.com/tmux/tmux) (required for server-managed sessions)
|
|
41
|
+
- [Claude Code](https://claude.ai/code) installed and authenticated
|
|
42
|
+
- `curl` and `jq` on your `PATH` (required for the permission relay hooks)
|
|
43
|
+
- Slack workspace admin access (to create and configure the Slack app)
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
### Environment Variables
|
|
50
|
+
|
|
51
|
+
Tokens and runtime options are read from environment variables. There is no `.env` file — export these in your shell profile.
|
|
52
|
+
|
|
53
|
+
| Variable | Description |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `SLACK_BOT_TOKEN` | Slack bot token (`xoxb-…`). Required. Granted by the OAuth install flow. |
|
|
56
|
+
| `SLACK_APP_TOKEN` | Slack app-level token (`xapp-…`). Required. Generated under Basic Information → App-Level Tokens with the `connections:write` scope. |
|
|
57
|
+
| `SLACK_STATE_DIR` | Override the directory where `routing.json`, `access.json`, and runtime state are stored. Defaults to `~/.claude/channels/slack`. |
|
|
58
|
+
| `SLACK_ACCESS_MODE` | Set to `static` to load `access.json` once at startup and cache it for the lifetime of the process rather than re-reading it on every event. Useful in high-throughput environments where disk reads are a concern. |
|
|
59
|
+
|
|
60
|
+
Shell profile example:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
export SLACK_BOT_TOKEN=xoxb-your-bot-token
|
|
64
|
+
export SLACK_APP_TOKEN=xapp-your-app-token
|
|
65
|
+
# Optional overrides:
|
|
66
|
+
export SLACK_STATE_DIR=~/.config/slack-channel-bots
|
|
67
|
+
export SLACK_ACCESS_MODE=static
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### Routing (routing.json)
|
|
73
|
+
|
|
74
|
+
`routing.json` is read from `~/.claude/channels/slack/routing.json` by default. Override the directory with `SLACK_STATE_DIR`.
|
|
75
|
+
|
|
76
|
+
A skeleton file is created by postinstall. Populate it before running `start`.
|
|
77
|
+
|
|
78
|
+
#### Complete example
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"routes": {
|
|
83
|
+
"C0123456789": { "cwd": "~/projects/alpha" },
|
|
84
|
+
"C9876543210": { "cwd": "~/projects/beta" }
|
|
85
|
+
},
|
|
86
|
+
"default_route": "~/projects/alpha",
|
|
87
|
+
"default_dm_session": "~/projects/alpha",
|
|
88
|
+
"bind": "127.0.0.1",
|
|
89
|
+
"port": 3100,
|
|
90
|
+
"session_restart_delay": 60,
|
|
91
|
+
"health_check_interval": 120,
|
|
92
|
+
"mcp_config_path": "~/.claude/slack-mcp.json"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Field reference
|
|
97
|
+
|
|
98
|
+
| Field | Type | Default | Description |
|
|
99
|
+
|---|---|---|---|
|
|
100
|
+
| `routes` | object | required | Map of Slack channel ID → route entry. Each entry requires a `cwd` field: the working directory for that session. Used to identify sessions via `roots/list` after MCP handshake. `~` is expanded. Each `cwd` must be unique across all routes. |
|
|
101
|
+
| `default_route` | string | — | CWD path to use when a message arrives on a channel with no explicit entry in `routes`. Must match an existing route `cwd`. |
|
|
102
|
+
| `default_dm_session` | string | — | CWD path of the session that handles direct messages. Must match an existing route `cwd`. |
|
|
103
|
+
| `bind` | string | `"127.0.0.1"` | Interface the HTTP server binds to. Use `"0.0.0.0"` to expose on all interfaces. |
|
|
104
|
+
| `port` | number | `3100` | Port the HTTP server listens on. |
|
|
105
|
+
| `session_restart_delay` | number | `60` | Seconds to wait before auto-restarting a dead session. Set to `0` to disable auto-restart. Must be non-negative. |
|
|
106
|
+
| `health_check_interval` | number | `120` | Seconds between periodic liveness polls. Set to `0` to disable. Must be non-negative. |
|
|
107
|
+
| `mcp_config_path` | string | `~/.claude/slack-mcp.json` | Path to the MCP config file passed to Claude Code when launching managed sessions. |
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### Access Control (access.json)
|
|
112
|
+
|
|
113
|
+
`access.json` is read from `~/.claude/channels/slack/access.json` by default (same directory as `routing.json`). A skeleton file with defaults is created by postinstall. The file is written with `0600` permissions.
|
|
114
|
+
|
|
115
|
+
Channels in `routing.json` are automatically allowed — you do not need to list them here. The `channels` map is only needed for per-channel overrides like requiring @mentions or restricting which users can trigger the bot.
|
|
116
|
+
|
|
117
|
+
The `slack-channel-access` skill manages pairings and allowlist entries at runtime.
|
|
118
|
+
|
|
119
|
+
#### Complete example
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"dmPolicy": "pairing",
|
|
124
|
+
"allowFrom": ["U0123456789"],
|
|
125
|
+
"channels": {
|
|
126
|
+
"C9876543210": {
|
|
127
|
+
"requireMention": true,
|
|
128
|
+
"allowFrom": ["U0123456789", "U9876543210"]
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"pending": {},
|
|
132
|
+
"ackReaction": "eyes",
|
|
133
|
+
"textChunkLimit": 3000,
|
|
134
|
+
"chunkMode": "newline"
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### Field reference
|
|
139
|
+
|
|
140
|
+
| Field | Type | Default | Description |
|
|
141
|
+
|---|---|---|---|
|
|
142
|
+
| `dmPolicy` | `"pairing"` \| `"allowlist"` \| `"disabled"` | `"pairing"` | Controls who can DM the bot. `pairing`: unknown users receive a one-time code and are added to `allowFrom` after verification. `allowlist`: only users in `allowFrom` are accepted. `disabled`: all DMs are dropped. |
|
|
143
|
+
| `allowFrom` | string[] | `[]` | Slack user IDs allowed to DM the bot unconditionally (regardless of `dmPolicy`). |
|
|
144
|
+
| `channels` | object | `{}` | Optional per-channel overrides. Channels in `routing.json` are allowed automatically — only add entries here to customize behavior (e.g. require @mention or restrict users). Each entry is a `ChannelPolicy`. |
|
|
145
|
+
| `channels[id].requireMention` | boolean | `false` | When `true`, messages in that channel are only delivered if the bot is `@mentioned`. |
|
|
146
|
+
| `channels[id].allowFrom` | string[] | `[]` | When non-empty, restricts delivery to the listed Slack user IDs for that channel. |
|
|
147
|
+
| `pending` | object | `{}` | Managed by the server. Stores in-flight pairing codes indexed by code string. Do not edit manually. |
|
|
148
|
+
| `ackReaction` | string | — | Emoji name (without colons) to react with when a message is received and dispatched. |
|
|
149
|
+
| `textChunkLimit` | number | — | Maximum character count per Slack message when chunking long replies. Controlled by the `reply` tool. |
|
|
150
|
+
| `chunkMode` | `"length"` \| `"newline"` | — | How to split overlong replies. `length`: hard split at `textChunkLimit` characters. `newline`: split at newline boundaries without exceeding `textChunkLimit`. |
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### MCP Server Config (slack-mcp.json)
|
|
155
|
+
|
|
156
|
+
Claude Code sessions need a config file pointing at the MCP server. A skeleton is created by postinstall at `~/.claude/slack-mcp.json`.
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"mcpServers": {
|
|
161
|
+
"slack-channel-router": {
|
|
162
|
+
"type": "http",
|
|
163
|
+
"url": "http://127.0.0.1:3100/mcp"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
If you changed `port` or `bind` in `routing.json`, update the `url` here to match. The server-managed session launcher uses `mcp_config_path` from `routing.json` to locate this file.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## CLI Reference
|
|
174
|
+
|
|
175
|
+
The `claude-slack-channel-bots` binary exposes two subcommands.
|
|
176
|
+
|
|
177
|
+
### `claude-slack-channel-bots start`
|
|
178
|
+
|
|
179
|
+
Checks prerequisites, then daemonizes the server.
|
|
180
|
+
|
|
181
|
+
**Prerequisite checks (in order):**
|
|
182
|
+
|
|
183
|
+
1. `tmux` is on `PATH` — fails with `missing prerequisite: tmux` if not found.
|
|
184
|
+
2. `SLACK_BOT_TOKEN` is set — fails with `missing prerequisite: SLACK_BOT_TOKEN environment variable` if absent.
|
|
185
|
+
3. `SLACK_APP_TOKEN` is set — fails with `missing prerequisite: SLACK_APP_TOKEN environment variable` if absent.
|
|
186
|
+
4. `routing.json` exists at `STATE_DIR/routing.json` — fails with the full path if not found.
|
|
187
|
+
|
|
188
|
+
If all checks pass, the parent process spawns a detached child process and exits immediately, printing the child PID. The child starts the server and writes its PID to `STATE_DIR/server.pid`.
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
[slack] Server starting in background (PID 12345)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### `claude-slack-channel-bots stop`
|
|
195
|
+
|
|
196
|
+
Reads `STATE_DIR/server.pid` and sends `SIGTERM` to the process.
|
|
197
|
+
|
|
198
|
+
Behavior by case:
|
|
199
|
+
|
|
200
|
+
- **PID file missing:** prints `server is not running` and exits 0.
|
|
201
|
+
- **Stale PID file** (process no longer running): removes the PID file, prints `server is not running (removed stale PID file)`, exits 0.
|
|
202
|
+
- **Live process:** sends `SIGTERM`, polls for exit every 100ms for up to 5 seconds. Prints `[slack] Server stopped.` on clean exit. Prints a warning if the server does not stop within 5 seconds (does not force-kill).
|
|
203
|
+
|
|
204
|
+
### PID file
|
|
205
|
+
|
|
206
|
+
The PID file is stored at `STATE_DIR/server.pid` (default: `~/.claude/channels/slack/server.pid`). It is written on startup and removed on clean shutdown. A conflict check at startup prevents running two servers against the same state directory.
|
|
207
|
+
|
|
208
|
+
### Direct invocation for development
|
|
209
|
+
|
|
210
|
+
Skip the CLI and run the server directly with Bun for development or debugging:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
bun server.ts
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
On startup the server prints the MCP endpoint and example config:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
[slack] Loaded routing config: 2 route(s)
|
|
220
|
+
[slack] Socket Mode connected
|
|
221
|
+
[slack] MCP server listening on http://127.0.0.1:3100/mcp
|
|
222
|
+
|
|
223
|
+
{
|
|
224
|
+
"mcpServers": {
|
|
225
|
+
"slack-channel-router": { "type": "http", "url": "http://127.0.0.1:3100/mcp" }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Tools
|
|
233
|
+
|
|
234
|
+
Each MCP endpoint exposes the following tools to the connected Claude Code session:
|
|
235
|
+
|
|
236
|
+
| Tool | Description |
|
|
237
|
+
|---|---|
|
|
238
|
+
| `reply` | Send a message to a Slack channel or DM. Auto-chunks long text according to `textChunkLimit` and `chunkMode` in `access.json`. Supports file attachments. |
|
|
239
|
+
| `react` | Add an emoji reaction to a Slack message. |
|
|
240
|
+
| `edit_message` | Edit a previously sent message (bot's own messages only). |
|
|
241
|
+
| `fetch_messages` | Fetch message history from a channel or thread. Returns oldest-first. |
|
|
242
|
+
| `download_attachment` | Download attachments from a Slack message. Saves files to `STATE_DIR/inbox/`. Returns local file paths. |
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Permission Relay
|
|
247
|
+
|
|
248
|
+
When Claude Code requires tool approval, the permission relay surfaces an interactive Slack message with **Allow** and **Deny** buttons instead of blocking the TUI. The Claude Code hook POSTs the pending request to the server, then long-polls for the user's response. Once the user clicks a button, the result is returned to Claude Code and execution continues.
|
|
249
|
+
|
|
250
|
+
The `ask-relay.sh` hook intercepts `AskUserQuestion` tool calls via `PreToolUse`, posts the question and its options to Slack as interactive buttons, and waits for the user's selection. The answer is returned to Claude Code via `updatedInput` without blocking the TUI.
|
|
251
|
+
|
|
252
|
+
Both hooks use a **two-phase long-poll protocol**:
|
|
253
|
+
|
|
254
|
+
1. **Phase 1 — Create request:** The hook POSTs to `/permission` (or `/ask`) with the tool name, input, and CWD. The server posts an interactive Slack message and returns a `requestId`.
|
|
255
|
+
2. **Phase 2 — Long-poll:** The hook GETs `/permission/{requestId}` (or `/ask/{requestId}`) in a loop with a 90-second `curl` timeout. The server holds the connection for up to 60 seconds waiting for a button click, then returns `{"status":"pending"}` if no decision has arrived. The hook retries immediately. Once the user clicks, the server returns `{"status":"decided","decision":"allow"|"deny"}` and the hook exits.
|
|
256
|
+
|
|
257
|
+
### Slack app prerequisites
|
|
258
|
+
|
|
259
|
+
The Slack app must have **interactivity enabled** with **Socket Mode** as the delivery method. Without this, button-click payloads are never delivered and the relay will not work.
|
|
260
|
+
|
|
261
|
+
To enable it: open your Slack app config → **Interactivity & Shortcuts** → toggle **Interactivity** on. No Request URL is needed — Socket Mode delivers interaction payloads over the existing socket connection. This is included automatically if you created the app from `slack-app-manifest.yml`.
|
|
262
|
+
|
|
263
|
+
### Hook installation
|
|
264
|
+
|
|
265
|
+
1. Copy the hook scripts from the repo to `~/.claude/hooks/`:
|
|
266
|
+
|
|
267
|
+
```sh
|
|
268
|
+
cp hooks/permission-relay.sh hooks/ask-relay.sh ~/.claude/hooks/
|
|
269
|
+
chmod +x ~/.claude/hooks/permission-relay.sh ~/.claude/hooks/ask-relay.sh
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Alternatively, symlink them so updates to the repo are reflected automatically:
|
|
273
|
+
|
|
274
|
+
```sh
|
|
275
|
+
ln -sf /path/to/repo/hooks/permission-relay.sh ~/.claude/hooks/permission-relay.sh
|
|
276
|
+
ln -sf /path/to/repo/hooks/ask-relay.sh ~/.claude/hooks/ask-relay.sh
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
2. Ensure `curl` and `jq` are on your `PATH`.
|
|
280
|
+
|
|
281
|
+
3. Add the following to your Claude Code `settings.json`:
|
|
282
|
+
|
|
283
|
+
```jsonc
|
|
284
|
+
"PermissionRequest": [
|
|
285
|
+
{
|
|
286
|
+
"matcher": ".*",
|
|
287
|
+
"timeout": 2000000,
|
|
288
|
+
"hooks": [{ "type": "command", "command": "~/.claude/hooks/permission-relay.sh" }]
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
"PreToolUse": [
|
|
292
|
+
{
|
|
293
|
+
"matcher": "AskUserQuestion",
|
|
294
|
+
"timeout": 2000000,
|
|
295
|
+
"hooks": [{ "type": "command", "command": "~/.claude/hooks/ask-relay.sh" }]
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`permission-relay.sh` relays tool permission requests (Allow/Deny) to Slack via `PermissionRequest`. `ask-relay.sh` relays `AskUserQuestion` calls to Slack via `PreToolUse`, returning the user's selection without blocking the TUI.
|
|
301
|
+
|
|
302
|
+
Both hooks auto-detect the server port from `routing.json`. They read `${SLACK_STATE_DIR:-$HOME/.claude/channels/slack}/routing.json` and use the `port` field (defaulting to `3100`), so they stay in sync if you change the port in routing config.
|
|
303
|
+
|
|
304
|
+
### Setup skill
|
|
305
|
+
|
|
306
|
+
The `update-config` skill can automate hook installation. It copies or symlinks the hooks and writes the `settings.json` entries in one step — use it if you prefer not to configure hooks manually.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Troubleshooting
|
|
311
|
+
|
|
312
|
+
**Missing environment variables**
|
|
313
|
+
`start` exits with `missing prerequisite: SLACK_BOT_TOKEN environment variable` or `SLACK_APP_TOKEN environment variable`. Export both tokens in your shell profile and open a new terminal before running `start`.
|
|
314
|
+
|
|
315
|
+
**routing.json not found**
|
|
316
|
+
`start` exits with `missing prerequisite: routing.json not found at <path>`. Run `bun postinstall.ts` to create a skeleton, or create the file manually. Verify `SLACK_STATE_DIR` matches the directory you populated.
|
|
317
|
+
|
|
318
|
+
**routing.json CWD mismatch**
|
|
319
|
+
If a Claude Code session connects but immediately disconnects, the session's actual CWD does not match any `cwd` in `routing.json`. Confirm the session's working directory matches the entry exactly (after tilde expansion). Duplicate CWDs across multiple routes are rejected at startup.
|
|
320
|
+
|
|
321
|
+
**Channel not in access.json**
|
|
322
|
+
Messages to channels not listed in `access.json → channels` are silently dropped. Use the `slack-channel-access` skill or edit `access.json` directly to add the channel ID with a `ChannelPolicy` entry.
|
|
323
|
+
|
|
324
|
+
**Permission relay not working**
|
|
325
|
+
Check that the Slack app has interactivity enabled (Interactivity & Shortcuts → toggle on). Verify `curl` and `jq` are on your `PATH`. Confirm the hook scripts are executable (`chmod +x`). If the port was changed in `routing.json`, ensure `SLACK_STATE_DIR` is set correctly so the hooks can read the updated port.
|
|
326
|
+
|
|
327
|
+
**Session not restarting after crash**
|
|
328
|
+
After 3 consecutive launch failures for a route, auto-restart is suspended until the server is restarted. Restart the server with `claude-slack-channel-bots stop && claude-slack-channel-bots start`. To disable auto-restart entirely, set `session_restart_delay` to `0` in `routing.json`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# AskUserQuestion relay — intercepts via PreToolUse, posts to Slack, returns answer
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
# Only handle AskUserQuestion
|
|
6
|
+
INPUT=$(cat)
|
|
7
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null) || exit 0
|
|
8
|
+
if [ "$TOOL_NAME" != "AskUserQuestion" ]; then
|
|
9
|
+
exit 0
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# Extract question and options from tool input
|
|
13
|
+
QUESTION=$(echo "$INPUT" | jq -r '.tool_input.question // ""' 2>/dev/null) || exit 0
|
|
14
|
+
OPTIONS=$(echo "$INPUT" | jq -c '.tool_input.options // []' 2>/dev/null) || exit 0
|
|
15
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || exit 0
|
|
16
|
+
|
|
17
|
+
if [ -z "$QUESTION" ] || [ "$OPTIONS" = "[]" ] || [ -z "$CWD" ]; then
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Read port from routing config
|
|
22
|
+
PORT=$(jq -r '.port // 3100' "${SLACK_STATE_DIR:-$HOME/.claude/channels/slack}/routing.json" 2>/dev/null) || PORT=3100
|
|
23
|
+
|
|
24
|
+
# Phase 1: POST question to server
|
|
25
|
+
RESPONSE=$(curl -s -f -X POST "http://127.0.0.1:${PORT}/ask" \
|
|
26
|
+
-H 'Content-Type: application/json' \
|
|
27
|
+
-d "{\"question\":$(printf '%s' "$QUESTION" | jq -Rs .),\"options\":${OPTIONS},\"cwd\":$(printf '%s' "$CWD" | jq -Rs .)}" \
|
|
28
|
+
2>/dev/null) || exit 0
|
|
29
|
+
|
|
30
|
+
REQUEST_ID=$(echo "$RESPONSE" | jq -r '.requestId // ""' 2>/dev/null) || exit 0
|
|
31
|
+
if [ -z "$REQUEST_ID" ]; then
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Phase 2: Long-poll for answer
|
|
36
|
+
while true; do
|
|
37
|
+
POLL_RESPONSE=$(curl -s -f --max-time 90 "http://127.0.0.1:${PORT}/ask/${REQUEST_ID}" 2>/dev/null) || exit 0
|
|
38
|
+
|
|
39
|
+
STATUS=$(echo "$POLL_RESPONSE" | jq -r '.status // ""' 2>/dev/null) || exit 0
|
|
40
|
+
|
|
41
|
+
if [ "$STATUS" = "decided" ]; then
|
|
42
|
+
ANSWER=$(echo "$POLL_RESPONSE" | jq -r '.answer // ""' 2>/dev/null) || exit 0
|
|
43
|
+
# Build the answers object: { "question text": "selected answer" }
|
|
44
|
+
ANSWERS_JSON=$(jq -n --arg q "$QUESTION" --arg a "$ANSWER" '{($q): $a}')
|
|
45
|
+
# Allow the tool and provide the answer via updatedInput
|
|
46
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","updatedInput":{"answers":%s}}}\n' "$ANSWERS_JSON"
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
# status=pending, retry
|
|
50
|
+
done
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# permission-relay.sh - Claude Code PermissionRequest hook
|
|
3
|
+
# Implements two-phase long-poll to relay permission decisions via Slack channel server
|
|
4
|
+
|
|
5
|
+
# Check dependencies
|
|
6
|
+
if ! command -v jq &>/dev/null; then
|
|
7
|
+
exit 0
|
|
8
|
+
fi
|
|
9
|
+
if ! command -v curl &>/dev/null; then
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
# Read stdin
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
|
|
16
|
+
# Extract fields from input
|
|
17
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null) || exit 0
|
|
18
|
+
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null) || exit 0
|
|
19
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || exit 0
|
|
20
|
+
|
|
21
|
+
# Read port from routing.json, default to 3100
|
|
22
|
+
ROUTING_FILE="${SLACK_STATE_DIR:-$HOME/.claude/channels/slack}/routing.json"
|
|
23
|
+
PORT=3100
|
|
24
|
+
if [ -f "$ROUTING_FILE" ]; then
|
|
25
|
+
ROUTED_PORT=$(jq -r '.port // empty' "$ROUTING_FILE" 2>/dev/null) || true
|
|
26
|
+
if [ -n "${ROUTED_PORT:-}" ]; then
|
|
27
|
+
PORT="$ROUTED_PORT"
|
|
28
|
+
fi
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
BASE_URL="http://127.0.0.1:${PORT}"
|
|
32
|
+
|
|
33
|
+
# Phase 1 — Create permission request
|
|
34
|
+
PAYLOAD=$(jq -n \
|
|
35
|
+
--arg tool_name "$TOOL_NAME" \
|
|
36
|
+
--argjson tool_input "$TOOL_INPUT" \
|
|
37
|
+
--arg cwd "$CWD" \
|
|
38
|
+
'{tool_name: $tool_name, tool_input: $tool_input, cwd: $cwd}' 2>/dev/null) || exit 0
|
|
39
|
+
|
|
40
|
+
RESPONSE=$(curl -s -f -X POST \
|
|
41
|
+
-H "Content-Type: application/json" \
|
|
42
|
+
-d "$PAYLOAD" \
|
|
43
|
+
--max-time 10 \
|
|
44
|
+
"${BASE_URL}/permission" 2>/dev/null) || exit 0
|
|
45
|
+
|
|
46
|
+
REQUEST_ID=$(echo "$RESPONSE" | jq -r '.requestId // ""' 2>/dev/null) || exit 0
|
|
47
|
+
if [ -z "$REQUEST_ID" ]; then
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Phase 2 — Long-poll loop (curl --max-time 90: 60s server hold + 30s buffer)
|
|
52
|
+
while true; do
|
|
53
|
+
POLL_RESPONSE=$(curl -s -f \
|
|
54
|
+
--max-time 90 \
|
|
55
|
+
"${BASE_URL}/permission/${REQUEST_ID}" 2>/dev/null) || exit 0
|
|
56
|
+
|
|
57
|
+
STATUS=$(echo "$POLL_RESPONSE" | jq -r '.status // ""' 2>/dev/null) || exit 0
|
|
58
|
+
|
|
59
|
+
case "$STATUS" in
|
|
60
|
+
"pending")
|
|
61
|
+
# Server is still holding; retry immediately
|
|
62
|
+
continue
|
|
63
|
+
;;
|
|
64
|
+
"decided")
|
|
65
|
+
BEHAVIOR=$(echo "$POLL_RESPONSE" | jq -r '.decision // ""' 2>/dev/null) || exit 0
|
|
66
|
+
if [ "$BEHAVIOR" = "allow" ]; then
|
|
67
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}\n'
|
|
68
|
+
else
|
|
69
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"Denied via Slack"}}}\n'
|
|
70
|
+
fi
|
|
71
|
+
exit 0
|
|
72
|
+
;;
|
|
73
|
+
*)
|
|
74
|
+
# Unknown status — fall through to TUI
|
|
75
|
+
exit 0
|
|
76
|
+
;;
|
|
77
|
+
esac
|
|
78
|
+
done
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-slack-channel-bots",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Multi-session Slack-to-Claude bridge — run multiple Claude Code bots across Slack channels via Socket Mode",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-slack-channel-bots": "./src/cli.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/*.ts",
|
|
11
|
+
"!src/*.test.ts",
|
|
12
|
+
"hooks/",
|
|
13
|
+
"slack-app-manifest.yml",
|
|
14
|
+
"README.md",
|
|
15
|
+
"skills/"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"bun": ">=1.0.0"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"postinstall": "bun src/postinstall.ts",
|
|
22
|
+
"start": "bun install --no-summary && bun src/server.ts",
|
|
23
|
+
"test": "bun test",
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
28
|
+
"@slack/socket-mode": "^2.0.0",
|
|
29
|
+
"@slack/web-api": "^7.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/bun": "^1.0.0",
|
|
33
|
+
"typescript": "^5.4.0"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"author": {
|
|
37
|
+
"name": "Gabe Mahoney"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/gabemahoney/claude-slack-channel-bots.git"
|
|
42
|
+
}
|
|
43
|
+
}
|