claude-slack-channel-bots 0.4.2 → 0.5.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/README.md CHANGED
@@ -56,7 +56,7 @@ Tokens and runtime options are read from environment variables. There is no `.en
56
56
  |---|---|
57
57
  | `SLACK_BOT_TOKEN` | Slack bot token (`xoxb-…`). Required. Granted by the OAuth install flow. |
58
58
  | `SLACK_APP_TOKEN` | Slack app-level token (`xapp-…`). Required. Generated under Basic Information → App-Level Tokens with the `connections:write` scope. |
59
- | `SLACK_STATE_DIR` | Override the directory where `routing.json`, `access.json`, and runtime state are stored. Defaults to `~/.claude/channels/slack`. |
59
+ | `SLACK_STATE_DIR` | Override the directory where `config.json`, `access.json`, and runtime state are stored. Defaults to `~/.claude/channels/slack`. |
60
60
  | `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. |
61
61
  | `SLACK_DRY_RUN` | Set to `1` to start the server without Slack credentials. Token validation is skipped, Socket Mode and `web.auth.test()` are not called, and MCP tool calls (`reply`, `react`, etc.) are logged instead of sent. Useful for integration testing. |
62
62
 
@@ -74,9 +74,9 @@ export SLACK_DRY_RUN=1
74
74
 
75
75
  ---
76
76
 
77
- ### Routing (routing.json)
77
+ ### Routing (config.json)
78
78
 
79
- `routing.json` is read from `~/.claude/channels/slack/routing.json` by default. Override the directory with `SLACK_STATE_DIR`.
79
+ `config.json` is read from `~/.claude/channels/slack/config.json` by default. Override the directory with `SLACK_STATE_DIR`.
80
80
 
81
81
  A skeleton file is created by postinstall. Populate it before running `start`.
82
82
 
@@ -105,7 +105,7 @@ A skeleton file is created by postinstall. Populate it before running `start`.
105
105
 
106
106
  | Field | Type | Default | Description |
107
107
  |---|---|---|---|
108
- | `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. |
108
+ | `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. May also include an optional `claude_config_dir` string (see below). |
109
109
  | `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`. Channels that are in `routes` but whose session is not yet registered have their messages dropped — they do not fall back to `default_route`. |
110
110
  | `default_dm_session` | string | — | CWD path of the session that handles direct messages. Must match an existing route `cwd`. |
111
111
  | `bind` | string | `"127.0.0.1"` | Interface the HTTP server binds to. Use `"0.0.0.0"` to expose on all interfaces. |
@@ -116,15 +116,40 @@ A skeleton file is created by postinstall. Populate it before running `start`.
116
116
  | `stop_timeout` | number | `30` | Seconds to wait for the server process to exit after `SIGTERM` before escalating to `SIGKILL`. |
117
117
  | `mcp_config_path` | string | `~/.claude/slack-mcp.json` | Path to the MCP config file passed to Claude Code when launching managed sessions. |
118
118
  | `append_system_prompt_file` | string | — | Path to a file appended to every managed session's system prompt via `--append-system-prompt-file`. Missing file silently skipped. See `skills/EXAMPLE_CLAUDE.md` for a template. |
119
+ | `system_prompt_mode` | string | `"append"` | Controls how `append_system_prompt_file` is applied. `"append"`: the custom prompt file is appended on top of `CLAUDE.md` (default, current behavior). `"none"`: only `CLAUDE.md` is used; `append_system_prompt_file` is ignored even if set. Use `"none"` when the project's `CLAUDE.md` already contains everything the bot needs. |
119
120
  | `cozempic_prescription` | string | `"standard"` | Cozempic cleaning intensity before resume. Valid values: `gentle`, `standard`, `aggressive`. Has no effect if cozempic is not installed. |
121
+ | `message_archive_db` | string | — | Path to a SQLite DB where every inbound Slack message is archived in real time. Parent directories are created if missing; schema is initialized on first open. Compatible with the `archive-messages.py` backfill script — both can write concurrently. Feature is disabled when absent. |
122
+ | `claude_config_dir` | string | — | Path to a Claude on-disk config directory. When set, managed sessions launch with `CLAUDE_CONFIG_DIR='<resolved-path>'` so the bot authenticates against a specific account. `~` is expanded and the path is resolved to absolute. Per-route `routes[id].claude_config_dir` overrides this top-level value for individual channels. When neither is set, Claude's own default applies. Must be non-empty when set. |
123
+ | `resume_enabled` | boolean | `true` | When `false`, the session manager always performs a fresh Claude session launch instead of resuming, both on startup and on runtime auto-restart, even when a stored session ID exists. Disabling this skips the `--resume` flag entirely. Use this as a workaround if your Claude Code version crashes with "sandbox required but unavailable" on `--resume` (a known regression in v2.1.120). |
124
+
125
+ #### Per-route `claude_config_dir` override
126
+
127
+ When you want different bot sessions to authenticate as different Claude accounts (e.g. one channel runs as a personal Max account, another as a corporate account), set `claude_config_dir` on the individual route. Per-route values take priority over the top-level `claude_config_dir`; routes without their own override fall back to the top-level value.
128
+
129
+ ```json
130
+ {
131
+ "routes": {
132
+ "C_PERSONAL": {
133
+ "cwd": "~/projects/alpha",
134
+ "claude_config_dir": "~/.claude-maxauth"
135
+ },
136
+ "C_CORPORATE": {
137
+ "cwd": "~/projects/beta"
138
+ }
139
+ },
140
+ "claude_config_dir": "~/.claude-corp"
141
+ }
142
+ ```
143
+
144
+ `C_PERSONAL` launches with the Max account; `C_CORPORATE` falls through to the top-level value and uses the corporate account. Use `claude auth login --claudeai` (or `--console`) with `CLAUDE_CONFIG_DIR` set to the same directory to populate each config dir before starting the server.
120
145
 
121
146
  ---
122
147
 
123
148
  ### Access Control (access.json)
124
149
 
125
- `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.
150
+ `access.json` is read from `~/.claude/channels/slack/access.json` by default (same directory as `config.json`). A skeleton file with defaults is created by postinstall. The file is written with `0600` permissions.
126
151
 
127
- 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.
152
+ Channels in `config.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.
128
153
 
129
154
  The `slack-channel-access` skill manages pairings and allowlist entries at runtime.
130
155
 
@@ -153,7 +178,7 @@ The `slack-channel-access` skill manages pairings and allowlist entries at runti
153
178
  |---|---|---|---|
154
179
  | `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. |
155
180
  | `allowFrom` | string[] | `[]` | Slack user IDs allowed to DM the bot unconditionally (regardless of `dmPolicy`). |
156
- | `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`. |
181
+ | `channels` | object | `{}` | Optional per-channel overrides. Channels in `config.json` are allowed automatically — only add entries here to customize behavior (e.g. require @mention or restrict users). Each entry is a `ChannelPolicy`. |
157
182
  | `channels[id].requireMention` | boolean | `false` | When `true`, messages in that channel are only delivered if the bot is `@mentioned`. |
158
183
  | `channels[id].allowFrom` | string[] | `[]` | When non-empty, restricts delivery to the listed Slack user IDs for that channel. |
159
184
  | `pending` | object | `{}` | Managed by the server. Stores in-flight pairing codes indexed by code string. Do not edit manually. |
@@ -178,7 +203,7 @@ Claude Code sessions need a config file pointing at the MCP server. A skeleton i
178
203
  }
179
204
  ```
180
205
 
181
- 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.
206
+ If you changed `port` or `bind` in `config.json`, update the `url` here to match. The server-managed session launcher uses `mcp_config_path` from `config.json` to locate this file.
182
207
 
183
208
  ---
184
209
 
@@ -195,7 +220,7 @@ Checks prerequisites, then daemonizes the server.
195
220
  1. `tmux` is on `PATH` — fails with `missing prerequisite: tmux` if not found.
196
221
  2. `SLACK_BOT_TOKEN` is set — fails with `missing prerequisite: SLACK_BOT_TOKEN environment variable` if absent.
197
222
  3. `SLACK_APP_TOKEN` is set — fails with `missing prerequisite: SLACK_APP_TOKEN environment variable` if absent.
198
- 4. `routing.json` exists at `STATE_DIR/routing.json` — fails with the full path if not found.
223
+ 4. `config.json` exists at `STATE_DIR/config.json` — fails with the full path if not found.
199
224
 
200
225
  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`. Conversation context is preserved across server restarts when possible.
201
226
 
@@ -221,7 +246,7 @@ Gracefully exits all managed Claude Code sessions, then stops and starts the ser
221
246
  claude-slack-channel-bots clean_restart
222
247
  ```
223
248
 
224
- For each session in `sessions.json`, sends `/exit` to the tmux session and polls until Claude exits. All sessions are processed in parallel. If a session does not exit within `exit_timeout` seconds (default 120s), its tmux session is force-killed. Individual session errors are logged and do not abort the restart. After the server restarts, sessions are relaunched with `--resume` using the stored session IDs in `sessions.json`, preserving conversation context.
249
+ For each session in `sessions.json`, sends `/exit` to the tmux session and polls until Claude exits. All sessions are processed in parallel. If a session does not exit within `exit_timeout` seconds (default 120s), its tmux session is force-killed. Individual session errors are logged and do not abort the restart. After the server restarts, sessions are relaunched using the stored session IDs in `sessions.json`. When `resume_enabled` is `true` (the default), sessions resume with `--resume`, preserving conversation context. When `resume_enabled` is `false`, sessions always launch fresh without `--resume`.
225
250
 
226
251
  Behavior by case:
227
252
 
@@ -270,17 +295,65 @@ Each MCP endpoint exposes the following tools to the connected Claude Code sessi
270
295
 
271
296
  ---
272
297
 
298
+ ## Interject
299
+
300
+ POST to `/interject` to inject a message into an active Claude session from localhost. Only requests from `127.0.0.1` or `::1` are accepted — external callers are rejected with 403.
301
+
302
+ ### Request
303
+
304
+ ```sh
305
+ curl -X POST http://localhost:<port>/interject \
306
+ -H "Content-Type: application/json" \
307
+ -d '{"channel": "C1234567890", "message": "Hello from a script", "sender": "my-cron-job"}'
308
+ ```
309
+
310
+ | Field | Required | Description |
311
+ |---|---|---|
312
+ | `channel` | yes | Slack channel ID matching an entry in `config.json → routes`. |
313
+ | `message` | yes | Text to inject into the session. |
314
+ | `sender` | no | Label attached to the injected message. Defaults to `"interject"`. |
315
+
316
+ ### Response
317
+
318
+ On success, returns HTTP 200:
319
+
320
+ ```json
321
+ { "ok": true, "channel": "C1234567890", "cwd": "/path/to/session" }
322
+ ```
323
+
324
+ ### Error conditions
325
+
326
+ | Status | Meaning |
327
+ |---|---|
328
+ | 400 | Invalid JSON or missing required field (`channel` or `message`). |
329
+ | 403 | Request did not originate from localhost. |
330
+ | 404 | Channel not found in `config.json → routes`. |
331
+ | 405 | Must use POST method. |
332
+ | 413 | Request body exceeds 32KB. |
333
+ | 503 | Channel is routed but no active session is connected. |
334
+
335
+ ### Example: crontab reminder
336
+
337
+ ```sh
338
+ # crontab -e
339
+ 0 9 * * 1 curl -s -X POST http://localhost:3100/interject \
340
+ -H "Content-Type: application/json" \
341
+ -d '{"channel": "C1234567890", "message": "Weekly reminder: update the changelog before standup.", "sender": "cron"}'
342
+ ```
343
+
344
+ ---
345
+
273
346
  ## Permission Relay
274
347
 
275
348
  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.
276
349
 
277
350
  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.
278
351
 
279
- Both hooks are **scope-guarded**: they check for the `SLACK_CHANNEL_BOT_SESSION` environment variable and exit immediately (no-op) if it is not set. The server sets this variable on every Claude session it launches. This means installing the hooks globally in `settings.json` is safe — they will not activate for Claude sessions you run outside the bot.
352
+ Both hooks are **scope-guarded**: they check the `CLAUDE_MANAGED_CHANNEL` environment variable, which is set as an inline env var (no `export`) on the `claude` command at launch. If the variable is absent, the hooks exit silently (no-op). This means installing the hooks globally in `settings.json` is safe — they will not activate for Claude sessions you run outside the bot. If the variable is present but the server is unreachable, hooks fail closed (deny the tool call) to prevent indefinite hangs in headless sessions.
280
353
 
281
354
  Both hooks use a **two-phase long-poll protocol**:
282
355
 
283
- 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`.
356
+ 1. **Phase 1 — Create request:** The hook POSTs to `/permission` (or `/ask`) with the tool name, input, and channel ID (from `$CLAUDE_MANAGED_CHANNEL`). The server posts an interactive Slack message and returns a `requestId`.
284
357
  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.
285
358
 
286
359
  ### Slack app prerequisites
@@ -328,7 +401,7 @@ To enable it: open your Slack app config → **Interactivity & Shortcuts** → t
328
401
 
329
402
  `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.
330
403
 
331
- 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.
404
+ Both hooks auto-detect the server port from `config.json`. They read `${SLACK_STATE_DIR:-$HOME/.claude/channels/slack}/config.json` and use the `port` field (defaulting to `3100`), so they stay in sync if you change the port in routing config.
332
405
 
333
406
  ### Setup skill
334
407
 
@@ -341,23 +414,33 @@ The `update-config` skill can automate hook installation. It copies or symlinks
341
414
  **Missing environment variables**
342
415
  `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`.
343
416
 
344
- **routing.json not found**
345
- `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.
417
+ **config.json not found**
418
+ `start` exits with `missing prerequisite: config.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.
346
419
 
347
- **routing.json CWD mismatch**
348
- 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.
420
+ **config.json CWD mismatch**
421
+ If a Claude Code session connects but immediately disconnects, the session's actual CWD does not match any `cwd` in `config.json`. Confirm the session's working directory matches the entry exactly (after tilde expansion). Duplicate CWDs across multiple routes are rejected at startup.
349
422
 
350
423
  **Bot not receiving messages in a new channel**
351
424
  After inviting the bot to a channel, Slack may not deliver messages until the bot is @mentioned for the first time. This is a Slack Socket Mode behavior — the first @mention activates event delivery for that channel. After that, all messages flow normally regardless of `requireMention` settings.
352
425
 
353
426
  **Channel not in access.json**
354
- Messages to channels not listed in `access.json → channels` and not present in `routing.json → routes` are silently dropped. Use the `claude-slack-channels-config` skill or edit `access.json` directly to add the channel ID with a `ChannelPolicy` entry.
427
+ Messages to channels not listed in `access.json → channels` and not present in `config.json → routes` are silently dropped. Use the `claude-slack-channels-config` skill or edit `access.json` directly to add the channel ID with a `ChannelPolicy` entry.
355
428
 
356
429
  **Permission relay not working**
357
- 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. If the hooks are silently doing nothing, confirm the session was launched by the server — the hooks only activate when `SLACK_CHANNEL_BOT_SESSION=1` is present in the environment. Sessions launched manually will not trigger the relay.
430
+ 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 `config.json`, ensure `SLACK_STATE_DIR` is set correctly so the hooks can read the updated port. If the hooks are silently doing nothing, confirm the session was launched by the server — the hooks check the `CLAUDE_MANAGED_CHANNEL` env var and exit silently if it is not set. Sessions launched manually will not have this variable and will not trigger the relay.
358
431
 
359
432
  **Session not restarting after crash**
360
- 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`.
433
+ 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 `config.json`.
361
434
 
362
435
  **Session stuck during clean_restart**
363
436
  If a session does not exit within `exit_timeout` seconds (default 120s), `clean_restart` force-kills its tmux session and proceeds. To manually recover, run `tmux kill-session -t <session-name>` for any remaining sessions, then `claude-slack-channel-bots stop && claude-slack-channel-bots start`.
437
+
438
+ **Session crashes on resume with "sandbox required but unavailable"**
439
+ This is a known regression in certain Claude Code releases (e.g. v2.1.120) where `--resume` triggers a sandbox check that fails in headless environments. Set `resume_enabled: false` in `config.json` to disable `--resume` entirely — the bot will always start a fresh Claude session instead of resuming a prior conversation, both on startup and on runtime auto-restart:
440
+
441
+ ```json
442
+ {
443
+ "routes": { ... },
444
+ "resume_enabled": false
445
+ }
446
+ ```
@@ -2,10 +2,27 @@
2
2
  # AskUserQuestion relay — intercepts via PreToolUse, posts to Slack, returns answer
3
3
  set -euo pipefail
4
4
 
5
+ # Check dependencies (must be before guard since guard needs curl+jq)
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 port from config.json
14
+ PORT=$(jq -r '.port // 3100' "${SLACK_STATE_DIR:-$HOME/.claude/channels/slack}/config.json" 2>/dev/null) || PORT=3100
15
+
5
16
  # Guard: only relay for bot-managed sessions
6
- if [ -z "${SLACK_CHANNEL_BOT_SESSION:-}" ]; then
17
+ if [ -z "${CLAUDE_MANAGED_CHANNEL:-}" ]; then
7
18
  exit 0
8
19
  fi
20
+ CHANNEL="$CLAUDE_MANAGED_CHANNEL"
21
+
22
+ deny_and_exit() {
23
+ printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny"}}\n'
24
+ exit 0
25
+ }
9
26
 
10
27
  # Only handle AskUserQuestion
11
28
  INPUT=$(cat)
@@ -15,36 +32,32 @@ if [ "$TOOL_NAME" != "AskUserQuestion" ]; then
15
32
  fi
16
33
 
17
34
  # Extract question and options from tool input
18
- QUESTION=$(echo "$INPUT" | jq -r '.tool_input.question // ""' 2>/dev/null) || exit 0
19
- OPTIONS=$(echo "$INPUT" | jq -c '.tool_input.options // []' 2>/dev/null) || exit 0
20
- CWD=$(echo "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || exit 0
35
+ QUESTION=$(echo "$INPUT" | jq -r '.tool_input.question // ""' 2>/dev/null) || deny_and_exit
36
+ OPTIONS=$(echo "$INPUT" | jq -c '.tool_input.options // []' 2>/dev/null) || deny_and_exit
21
37
 
22
- if [ -z "$QUESTION" ] || [ "$OPTIONS" = "[]" ] || [ -z "$CWD" ]; then
23
- exit 0
38
+ if [ -z "$QUESTION" ] || [ "$OPTIONS" = "[]" ]; then
39
+ deny_and_exit
24
40
  fi
25
41
 
26
- # Read port from routing config
27
- PORT=$(jq -r '.port // 3100' "${SLACK_STATE_DIR:-$HOME/.claude/channels/slack}/routing.json" 2>/dev/null) || PORT=3100
28
-
29
42
  # Phase 1: POST question to server
30
43
  RESPONSE=$(curl -s -f -X POST "http://127.0.0.1:${PORT}/ask" \
31
44
  -H 'Content-Type: application/json' \
32
- -d "{\"question\":$(printf '%s' "$QUESTION" | jq -Rs .),\"options\":${OPTIONS},\"cwd\":$(printf '%s' "$CWD" | jq -Rs .)}" \
33
- 2>/dev/null) || exit 0
45
+ -d "{\"question\":$(printf '%s' "$QUESTION" | jq -Rs .),\"options\":${OPTIONS},\"channel\":$(printf '%s' "$CHANNEL" | jq -Rs .)}" \
46
+ 2>/dev/null) || deny_and_exit
34
47
 
35
- REQUEST_ID=$(echo "$RESPONSE" | jq -r '.requestId // ""' 2>/dev/null) || exit 0
48
+ REQUEST_ID=$(echo "$RESPONSE" | jq -r '.requestId // ""' 2>/dev/null) || deny_and_exit
36
49
  if [ -z "$REQUEST_ID" ]; then
37
- exit 0
50
+ deny_and_exit
38
51
  fi
39
52
 
40
53
  # Phase 2: Long-poll for answer
41
54
  while true; do
42
- POLL_RESPONSE=$(curl -s -f --max-time 90 "http://127.0.0.1:${PORT}/ask/${REQUEST_ID}" 2>/dev/null) || exit 0
55
+ POLL_RESPONSE=$(curl -s -f --max-time 90 "http://127.0.0.1:${PORT}/ask/${REQUEST_ID}" 2>/dev/null) || deny_and_exit
43
56
 
44
- STATUS=$(echo "$POLL_RESPONSE" | jq -r '.status // ""' 2>/dev/null) || exit 0
57
+ STATUS=$(echo "$POLL_RESPONSE" | jq -r '.status // ""' 2>/dev/null) || deny_and_exit
45
58
 
46
59
  if [ "$STATUS" = "decided" ]; then
47
- ANSWER=$(echo "$POLL_RESPONSE" | jq -r '.answer // ""' 2>/dev/null) || exit 0
60
+ ANSWER=$(echo "$POLL_RESPONSE" | jq -r '.answer // ""' 2>/dev/null) || deny_and_exit
48
61
  # Build the answers object: { "question text": "selected answer" }
49
62
  ANSWERS_JSON=$(jq -n --arg q "$QUESTION" --arg a "$ANSWER" '{($q): $a}')
50
63
  # Allow the tool and provide the answer via updatedInput
@@ -2,11 +2,6 @@
2
2
  # permission-relay.sh - Claude Code PermissionRequest hook
3
3
  # Implements two-phase long-poll to relay permission decisions via Slack channel server
4
4
 
5
- # Guard: only relay for bot-managed sessions
6
- if [ -z "${SLACK_CHANNEL_BOT_SESSION:-}" ]; then
7
- exit 0
8
- fi
9
-
10
5
  # Check dependencies
11
6
  if ! command -v jq &>/dev/null; then
12
7
  exit 0
@@ -15,51 +10,62 @@ if ! command -v curl &>/dev/null; then
15
10
  exit 0
16
11
  fi
17
12
 
18
- # Read stdin
19
- INPUT=$(cat)
20
-
21
- # Extract fields from input
22
- TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null) || exit 0
23
- TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null) || exit 0
24
- CWD=$(echo "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || exit 0
25
-
26
- # Read port from routing.json, default to 3100
27
- ROUTING_FILE="${SLACK_STATE_DIR:-$HOME/.claude/channels/slack}/routing.json"
13
+ # Read port from config.json, default to 3100
14
+ CONFIG_FILE="${SLACK_STATE_DIR:-$HOME/.claude/channels/slack}/config.json"
28
15
  PORT=3100
29
- if [ -f "$ROUTING_FILE" ]; then
30
- ROUTED_PORT=$(jq -r '.port // empty' "$ROUTING_FILE" 2>/dev/null) || true
16
+ if [ -f "$CONFIG_FILE" ]; then
17
+ ROUTED_PORT=$(jq -r '.port // empty' "$CONFIG_FILE" 2>/dev/null) || true
31
18
  if [ -n "${ROUTED_PORT:-}" ]; then
32
19
  PORT="$ROUTED_PORT"
33
20
  fi
34
21
  fi
35
22
 
23
+ # Guard: only relay for bot-managed sessions
24
+ if [ -z "${CLAUDE_MANAGED_CHANNEL:-}" ]; then
25
+ printf '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"Not a managed Slack session"}}}\n'
26
+ exit 0
27
+ fi
28
+ CHANNEL="$CLAUDE_MANAGED_CHANNEL"
29
+
30
+ deny_and_exit() {
31
+ printf '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"%s"}}}\n' "$1"
32
+ exit 0
33
+ }
34
+
35
+ # Read stdin
36
+ INPUT=$(cat)
37
+
38
+ # Extract fields from input
39
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null) || deny_and_exit "Failed to parse tool_name"
40
+ TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null) || deny_and_exit "Failed to parse tool_input"
41
+
36
42
  BASE_URL="http://127.0.0.1:${PORT}"
37
43
 
38
44
  # Phase 1 — Create permission request
39
45
  PAYLOAD=$(jq -n \
40
46
  --arg tool_name "$TOOL_NAME" \
41
47
  --argjson tool_input "$TOOL_INPUT" \
42
- --arg cwd "$CWD" \
43
- '{tool_name: $tool_name, tool_input: $tool_input, cwd: $cwd}' 2>/dev/null) || exit 0
48
+ --arg channel "$CHANNEL" \
49
+ '{tool_name: $tool_name, tool_input: $tool_input, channel: $channel}' 2>/dev/null) || deny_and_exit "Failed to build payload"
44
50
 
45
51
  RESPONSE=$(curl -s -f -X POST \
46
52
  -H "Content-Type: application/json" \
47
53
  -d "$PAYLOAD" \
48
54
  --max-time 10 \
49
- "${BASE_URL}/permission" 2>/dev/null) || exit 0
55
+ "${BASE_URL}/permission" 2>/dev/null) || deny_and_exit "Server unreachable"
50
56
 
51
- REQUEST_ID=$(echo "$RESPONSE" | jq -r '.requestId // ""' 2>/dev/null) || exit 0
57
+ REQUEST_ID=$(echo "$RESPONSE" | jq -r '.requestId // ""' 2>/dev/null) || deny_and_exit "Failed to parse response"
52
58
  if [ -z "$REQUEST_ID" ]; then
53
- exit 0
59
+ deny_and_exit "No requestId in server response"
54
60
  fi
55
61
 
56
62
  # Phase 2 — Long-poll loop (curl --max-time 90: 60s server hold + 30s buffer)
57
63
  while true; do
58
64
  POLL_RESPONSE=$(curl -s -f \
59
65
  --max-time 90 \
60
- "${BASE_URL}/permission/${REQUEST_ID}" 2>/dev/null) || exit 0
66
+ "${BASE_URL}/permission/${REQUEST_ID}" 2>/dev/null) || deny_and_exit "Server connection lost"
61
67
 
62
- STATUS=$(echo "$POLL_RESPONSE" | jq -r '.status // ""' 2>/dev/null) || exit 0
68
+ STATUS=$(echo "$POLL_RESPONSE" | jq -r '.status // ""' 2>/dev/null) || deny_and_exit "Failed to parse response"
63
69
 
64
70
  case "$STATUS" in
65
71
  "pending")
@@ -67,7 +73,7 @@ while true; do
67
73
  continue
68
74
  ;;
69
75
  "decided")
70
- BEHAVIOR=$(echo "$POLL_RESPONSE" | jq -r '.decision // ""' 2>/dev/null) || exit 0
76
+ BEHAVIOR=$(echo "$POLL_RESPONSE" | jq -r '.decision // ""' 2>/dev/null) || deny_and_exit "Failed to parse decision"
71
77
  if [ "$BEHAVIOR" = "allow" ]; then
72
78
  printf '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}\n'
73
79
  else
@@ -76,8 +82,8 @@ while true; do
76
82
  exit 0
77
83
  ;;
78
84
  *)
79
- # Unknown status — fall through to TUI
80
- exit 0
85
+ # Unknown status — deny to fail closed
86
+ deny_and_exit "Unexpected poll status: $STATUS"
81
87
  ;;
82
88
  esac
83
89
  done
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-slack-channel-bots",
3
- "version": "0.4.2",
4
- "description": "Multi-session Slack-to-Claude bridge \u2014 run multiple Claude Code bots across Slack channels via Socket Mode",
3
+ "version": "0.5.1",
4
+ "description": "Multi-session Slack-to-Claude bridge run multiple Claude Code bots across Slack channels via Socket Mode",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "claude-slack-channel-bots": "src/cli.ts"
@@ -33,7 +33,7 @@ message handling (ack reactions, chunking).
33
33
  ```bash
34
34
  STATE_DIR="${SLACK_STATE_DIR:-$HOME/.claude/channels/slack}"
35
35
  # access.json lives at $STATE_DIR/access.json
36
- # routing.json lives at $STATE_DIR/routing.json
36
+ # config.json lives at $STATE_DIR/config.json
37
37
  ```
38
38
 
39
39
  Always resolve the state directory using `$SLACK_STATE_DIR` with fallback to
@@ -102,12 +102,12 @@ Parse `$ARGUMENTS` and execute the matching subcommand:
102
102
 
103
103
  ### `status`
104
104
  1. Load `access.json`
105
- 2. Load `routing.json` from the same state directory
105
+ 2. Load `config.json` from the same state directory
106
106
  3. Display:
107
107
  - DM policy
108
108
  - Allowlisted user IDs
109
109
  - Opted-in channels with their policies, showing two categories:
110
- - **Implicit** — channels present in `routing.json` routes (automatically opted-in)
110
+ - **Implicit** — channels present in `config.json` routes (automatically opted-in)
111
111
  - **Explicit** — channels configured in `access.json` channels (with their `requireMention` and `allowFrom` settings)
112
112
  - Pending pairings (code + sender ID + expiry)
113
113
  - Ack reaction setting (or "not set")
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: setup-slack-channel-bots
3
- description: Interactive setup wizard for claude-slack-channel-bots — checks tokens, routing.json, access.json, hooks, and Claude Code settings
3
+ description: Interactive setup wizard for claude-slack-channel-bots — checks tokens, config.json, access.json, hooks, and Claude Code settings
4
4
  version: 1.0.0
5
5
  author: Jeremy Longshore <jeremy@intentsolutions.io>
6
6
  license: MIT
@@ -122,14 +122,14 @@ want confirmation.
122
122
 
123
123
  ---
124
124
 
125
- ### Step 4 — Check routing.json
125
+ ### Step 4 — Check config.json
126
126
 
127
127
  State directory defaults to `~/.claude/channels/slack/`. Respect
128
128
  `$SLACK_STATE_DIR` if set.
129
129
 
130
130
  ```bash
131
131
  STATE_DIR="${SLACK_STATE_DIR:-$HOME/.claude/channels/slack}"
132
- cat "$STATE_DIR/routing.json" 2>/dev/null || echo "NOT_FOUND"
132
+ cat "$STATE_DIR/config.json" 2>/dev/null || echo "NOT_FOUND"
133
133
  ```
134
134
 
135
135
  **If the file does not exist:**
@@ -169,7 +169,7 @@ test -d "<expanded-path>" && echo "ok" || echo "not found"
169
169
  Expand `~` before checking. If the directory does not exist, warn the user and
170
170
  ask whether to proceed anyway or provide a different path.
171
171
 
172
- After collecting at least one route, write the updated `routing.json`.
172
+ After collecting at least one route, write the updated `config.json`.
173
173
 
174
174
  **Important:** Remind the user to invite the bot to each channel they configured.
175
175
  In Slack, type `/invite @Claude Slack Channel Bots` in each channel (or whatever
@@ -210,7 +210,7 @@ either value:
210
210
 
211
211
  **Validation before writing:**
212
212
 
213
- Before writing `routing.json`, verify that any value supplied for
213
+ Before writing `config.json`, verify that any value supplied for
214
214
  `default_route` or `default_dm_session` exactly matches one of the `cwd`
215
215
  values in `routes`. If a value does not match:
216
216
 
@@ -221,7 +221,7 @@ values in `routes`. If a value does not match:
221
221
 
222
222
  Only write the field once a valid value is confirmed.
223
223
 
224
- Write the final `routing.json` with only the fields the user explicitly set
224
+ Write the final `config.json` with only the fields the user explicitly set
225
225
  (plus the required `routes`). Do not write optional fields the user left at
226
226
  their defaults unless asked.
227
227
 
@@ -230,7 +230,7 @@ their defaults unless asked.
230
230
  ### Step 5 — Configure custom system prompt for worker sessions
231
231
 
232
232
  Worker sessions launched by the server can receive a custom system prompt
233
- via `append_system_prompt_file` in `routing.json`. This controls how the
233
+ via `append_system_prompt_file` in `config.json`. This controls how the
234
234
  bots behave — their role, communication style, and capabilities.
235
235
 
236
236
  **This is important.** Without a system prompt, bots won't know to communicate
@@ -275,7 +275,7 @@ Examples to offer if they're unsure:
275
275
  STATE_DIR="${SLACK_STATE_DIR:-$HOME/.claude/channels/slack}"
276
276
  ```
277
277
 
278
- 3. Read `routing.json`, add or update:
278
+ 3. Read `config.json`, add or update:
279
279
  ```json
280
280
  "append_system_prompt_file": "~/.claude/channels/slack/system-prompt.md"
281
281
  ```
@@ -286,9 +286,22 @@ Examples to offer if they're unsure:
286
286
 
287
287
  **If the user explicitly skips:**
288
288
 
289
- Do not write `append_system_prompt_file` to `routing.json`, but warn them
289
+ Do not write `append_system_prompt_file` to `config.json`, but warn them
290
290
  that bots won't know to communicate via Slack without a system prompt.
291
291
 
292
+ If the project's `CLAUDE.md` already contains all the instructions the bot
293
+ needs (including Slack communication directives), the user can opt out of a
294
+ separate prompt file by setting `system_prompt_mode: "none"` in `config.json`:
295
+
296
+ ```json
297
+ "system_prompt_mode": "none"
298
+ ```
299
+
300
+ With this setting, `append_system_prompt_file` is ignored even if present, and
301
+ only `CLAUDE.md` is used. This is the recommended approach when the bot's
302
+ behavior is fully defined in the project's `CLAUDE.md` and a separate
303
+ `system-prompt.md` would be redundant.
304
+
292
305
  ---
293
306
 
294
307
  ### Step 6 — Check access.json
@@ -437,7 +450,7 @@ Print a final summary of what was checked and configured:
437
450
 
438
451
  - Environment variables: set / missing
439
452
  - Token format: valid / invalid
440
- - routing.json: populated (N routes) / skeleton
453
+ - config.json: populated (N routes) / skeleton
441
454
  - append_system_prompt_file: configured / skipped
442
455
  - access.json: present / missing
443
456
  - permission-relay.sh hook: present and executable / missing
package/src/cli.ts CHANGED
@@ -105,11 +105,11 @@ export function createCli(deps: CliDeps): CliHandlers {
105
105
  }
106
106
  }
107
107
 
108
- // Check routing.json exists
108
+ // Check config.json exists
109
109
  const stateDir = deps.resolveStateDir()
110
- const routingJson = join(stateDir, 'routing.json')
110
+ const routingJson = join(stateDir, 'config.json')
111
111
  if (!deps.existsSync(routingJson)) {
112
- console.error(`missing prerequisite: routing.json not found at ${routingJson}`)
112
+ console.error(`missing prerequisite: config.json not found at ${routingJson}`)
113
113
  deps.exit(1)
114
114
  }
115
115