Route RiskKernel's human-in-the-loop gate to Slack: when a governed, side-effecting tool call needs sign-off, RiskKernel posts it to a channel with Approve / Deny buttons, and the click resolves the pending action — no terminal, no dashboard.
It's a push channel alongside the CLI, the local web page, and the webhook; the same
deterministic policy decides what needs approval (the LLM never does). Approving
from Slack is identical to POST /v1/runs/{id}/approve — Slack is just the transport.
- Create a Slack app (https://api.slack.com/apps → From scratch), add it to your workspace, and invite its bot user to the channel you want approvals in.
- Bot token — under OAuth & Permissions, add the
chat:writebot scope, install the app, and copy the Bot User OAuth Token (xoxb-…). - Interactivity — under Interactivity & Shortcuts, turn it on and set the
Request URL to your daemon's public endpoint:
https://<your-riskkernel-host>/v1/integrations/slack/interactions - Signing secret — copy it from Basic Information → App Credentials. RiskKernel verifies every interaction against this, so a forged button click is rejected.
Then point the daemon at it:
export RISKKERNEL_APPROVAL_SLACK_BOT_TOKEN=xoxb-… # chat:write
export RISKKERNEL_APPROVAL_SLACK_CHANNEL=C0123456789 # channel id
export RISKKERNEL_APPROVAL_SLACK_SIGNING_SECRET=… # verifies button clicksOn startup you'll see slack approval channel enabled. The channel id is the C…
from channel → View details, not the #name.
A gated call posts a message like:
Approval required Tool:
mcp://shellSide effect:execRun:4f3c…Step: 7 Arguments:{ "cmd": "rm -rf /tmp/cache" }[ Approve ] [ Deny ]
Click Approve and the blocked call proceeds; click Deny and it's refused. The
message is rewritten to ✅ Approved by slack:you (or 🛑 Denied) so it can't be
actioned twice. The decision is recorded in the audit trail with by = slack:<user>.
- The bot token and signing secret are secrets — supply them via the environment
(or your secret manager); they are never logged and never leave the process except
as the
Authorizationheader to Slack's API. - The interactivity callback is the one route not behind the daemon API token
(Slack can't send it). It's authenticated by the Slack request signature
instead — verified over the raw request body with a 5-minute replay window, and it
fails closed if no signing secret is configured. See
SECURITY.md. - It needs a URL Slack can reach. For local testing, front the daemon with a tunnel
(e.g.
ngrok); in production, your normal ingress. Keep the rest of the API behind your network boundary / the API token as usual.
- A pending approval still respects the run's time budget and the kill switch: if the run is cancelled or runs out of time while waiting, the call is refused even if no one clicked. Slack is a transport, not a separate timeout.
- The webhook (
RISKKERNEL_APPROVAL_WEBHOOK) and Slack can be enabled together — a pending approval fans out to both.