Channels
Slack Action Token
Keep the Slack action_token so an OpenClaw plugin can use Slack's Real-time Search API with a bot token
Install
openclaw plugins install ~/plugins/slack-patched
Configuration Example
ctx.app.event("message", async ({ event, body }) => {
__openclawSlackActionTokenCapture(event, body); // ← added
await handleIncomingMessageEvent({ event, body });
});
README
# openclaw-slack-action-token
Teaches [`@openclaw/slack`](https://www.npmjs.com/package/@openclaw/slack) to keep
the Slack `action_token`, so an OpenClaw plugin can call Slack's Real-time Search
API (`assistant.search.context`) with the workspace's **bot** token.
Verified end to end on OpenClaw 2026.6.34 / `@openclaw/slack` 2026.6.34.
## Why this is needed
Slack hands AI apps a short-lived `action_token` inside inbound event payloads.
It is the only way a bot token may search:
> All API calls made using a bot token require an `action_token`.
> — [assistant.search.context](https://docs.slack.dev/reference/methods/assistant.search.context)
The token arrives in `message.im` (a DM needs no mention), and in
`message.channels` / `message.groups` / `message.mpim` / `app_mention` when the
app is mentioned. The stock plugin never reads the field — zero occurrences of
`action_token` in the published 2026.6.34 bundle — so it is gone before any tool
runs, and `assistant.search.context` answers `invalid_action_token`.
The alternative, `search.messages`, is user-token only (`not_allowed_token_type`
for a bot token) and cannot be unlocked with extra scopes.
## What it does
Two call sites gain one line each, and one helper is appended:
```js
ctx.app.event("message", async ({ event, body }) => {
__openclawSlackActionTokenCapture(event, body); // ← added
await handleIncomingMessageEvent({ event, body });
});
```
The helper stores the newest token per `channel:user` on
`globalThis.__openclawSlackActionTokens`. A global rather than a plugin API
because the consumer is a different plugin in the same gateway process, and a
global is the one seam that cannot drift with an SDK change.
Total diff: under 30 lines, in one file. That is deliberate — every line has to
be re-applied to the next release.
## Install
```bash
# 1. copy the installed package (find it with `openclaw plugins list`)
cp -r ~/.openclaw/npm/projects/…/node_modules/@openclaw/slack ~/plugins/slack-patched
# 2. patch the copy (refuses loudly if upstream moved the handlers)
node patch.mjs ~/plugins/slack-patched --check # dry run
node patch.mjs ~/plugins/slack-patched
# 3. load the patched build instead of the stock one
openclaw plugins install ~/plugins/slack-patched --link
openclaw gateway restart
```
`--link` points at the directory, so re-patching after an upgrade needs no
reinstall. `--force` is rejected together with `--link`; that is expected.
Confirm it is live — the first captured token logs once per process:
```
[slack-action-token] capture active
```
Roll back with `openclaw plugins install @openclaw/slack@<version> --force`.
## Upgrading
The bundle filename carries a content hash, so the patcher finds the file by
content, never by name. If upstream renames or restructures a handler, the
patcher exits non-zero and says so rather than producing a silent no-op: a
quietly unpatched plugin is indistinguishable from "search stopped working".
```bash
node patch.mjs <new copy> --check && node patch.mjs <new copy>
```
If it refuses, re-read `ANCHORS` in `lib/patch.mjs` against the new bundle.
## Consuming the token
`lib/read-token.mjs` is dependency-free; copy it or import it.
```js
import { readActionToken } from "./read-token.mjs";
const token = readActionToken(channelId, userId); // null when absent or stale
```
**Pick the right person's token.** The token decides *whose* Slack permissions
the search runs under — Slack returns only what that user can see. Handing over
someone else's token silently searches as them. In a DM the session identifies
the user; in a channel it does not, so a consumer should refuse when more than
one person has spoken recently rather than guess. A worked example lives in
[`openclaw-slack-acl`](https://github.com/openclaw/openclaw) — see
`lib/action-token.mjs` there.
## Two things worth knowing before you build on this
**The token is tied to an interaction.** It arrives with a message and expires.
Search works while a person is talking to the bot; scheduled or background
search does not, and no amount of caching fixes that.
**`assistant.search.context` is semantic, not literal.** Measured on a live
workspace: the phrase `маркер для проверки поиска` returned 20 hits, while the
unique codeword sitting in the very same message returned zero. Pass the user's
question in their own words. Tests built on random codewords — the usual way to
prove isolation — will fail here for reasons that have nothing to do with
permissions.
## Tests
```bash
node --test test/patch.test.mjs
```
Covers both call sites being patched, the helper appearing once, refusal on a
missing or duplicated anchor, refusal to double-patch, the capture storing and
ignoring malformed payloads, and staleness.
## Licence
MIT.
channels
Comments
Sign in to leave a comment