How to Give a Slack AI Bot Each User's Own Level of Access
Give a Slack AI bot per-user access: resolve who is speaking, check their scopes before every tool call, and call your APIs with their own credentials.

Short answer: To give a Slack AI bot each user's own level of access, keep the bot token for reading and posting in Slack, and decide every action by the person speaking. Resolve each Slack user to an account in your system, check that person's scopes before any tool runs, and call your APIs with credentials issued to that person, never one shared admin key.
Maya runs finance at a 40-person company. She asks the agent in #billing to move a customer to the annual plan, and it does. Ten minutes later, an intern in the same channel types "@agent refund the last three invoices for Acme." The agent has the same billing tool and the same API key it used for Maya. Nothing in its setup knows the difference between the two of them, so it runs the refunds.
That is the default shape of most Slack bots. They are installed once, with one token and one set of API keys, and every person who can mention them gets everything the bot can do. This guide explains why that breaks, and how to build a Slack agent where each person gets exactly their own level of access. You'll learn:
- Why a shared bot token is fine for Slack but wrong for your tools
- What the confused-deputy problem is, and how prompt injection makes it worse
- The three pieces of a per-user setup: identity, scopes and per-user credentials
- How to set it up on Keiki in four steps
If you haven't put an agent in Slack yet, start with how to add an AI agent to Slack, the guide this one builds on.
Two kinds of tokens, two different jobs
Slack gives an app two main kinds of tokens. Slack's own token documentation says bot tokens "represent a bot associated with an app installed in a workspace" and are "not tied to a user's identity." User tokens "represent workspace members" and carry the same access that person has to the workspace.
For the Slack side of an agent, a bot token is the right choice. The agent reads the channels it was invited to, posts replies and keeps working when the person who installed it leaves. Slack recommends bot tokens for exactly that reason.
The trouble starts one layer down, in your own systems. When the agent calls your billing API, your CRM or your database, it needs a credential there too. The easy path is one service key with admin rights, stored next to the bot token. Now the agent acts with the widest access anyone in the company has, on behalf of whoever happens to be typing.
The confused deputy in your Slack channel
Security people have a name for this. AWS describes the confused deputy problem as "a security issue where an entity that doesn't have permission to perform an action can coerce a more-privileged entity to perform the action." The intern can't issue refunds. The agent can. The intern asks the agent.
The OWASP GenAI Security Project lists this pattern in its 2025 Top 10 for LLM applications as LLM06: Excessive Agency. One of its examples is almost word for word the Slack bot above: an extension "designed to perform operations in the context of an individual user accesses downstream systems with a generic high-privileged identity." OWASP's fix is to run those calls in the user's own context, with the minimum privileges they need.
The Model Context Protocol has reached the same conclusion for tool servers. Its security best practices cover confused-deputy attacks and forbid "token passthrough," where a server forwards a token it never checked was issued to it.
Prompt injection makes it worse
A model can be talked into things. In August 2024, the security firm PromptArmor showed that Slack AI could be tricked into leaking data from private channels. An attacker posted instructions in a public channel, and when a victim later asked Slack AI a question, the answer included a link that carried the victim's private API key to the attacker. The Register reported on August 21, 2024 that Salesforce investigated and deployed a patch, and said it had no evidence of unauthorized access to customer data.
The lesson for anyone building a Slack agent: you can't rely on the model to refuse. If a message, a file or a pasted webpage can steer the agent, the only safe limit is one the model can't move. That means permissions checked outside the model, against a person the platform identified, not one the model was told about.
What per-user access needs
A Slack agent that respects each person's access needs three things.
- A verified speaker. Who sent this message, according to Slack, not according to the message text. Slack signs every request it sends with your app's signing secret, so the user id on a verified event is trustworthy. A name typed into the chat is not.
- That person's permissions in your system. Slack knows who someone is in Slack. Only your product knows whether that person may refund invoices. You need a way to map the Slack user to your own user, role and scopes.
- That person's credentials for your tools. When the agent calls an API on their behalf, it should use a token issued to them, so your API applies its own rules too. If the model is fooled, the worst it can do is what that person could already do.
How Keiki resolves who is talking
On Keiki, the Slack app's bot token handles Slack: reading the channels it was invited to and posting replies. Everything about who may do what comes from the person speaking.
When a message arrives, Keiki checks Slack's signature first. It then takes the sender from the verified event: the Slack team id and user id. That pair becomes the turn's principal, the speaker every check is run against. The model never chooses it, and a message can't change it.
To learn what that person may do, Keiki calls an identity webhook you run. It sends the Slack team id, user id and display name (and the email, if your app has the users:read.email scope), signed with HMAC-SHA256 under your secret. You answer with your own user id, role and scopes. Keiki caches the answer for the ttlSeconds you return, 15 minutes by default.
Three details matter for security:
- Failures are visible, never "allow." If your webhook errors or times out, the turn still runs, but every tool that needs a scope is refused and the agent tells the person it could not verify who they are.
- Unknown people get no scopes. Someone your system doesn't know still gets answers to general questions, but every gated tool stays out of reach.
- The model sees names and scopes, not secrets. The agent knows the speaker's name, role and scopes, so it can answer "can I do this?" without trying. It never sees an email, a token or a header value.
The Slack agents docs cover the rest of the speaker model, including how the agent notices when a new person joins a thread.
How to give each Slack user their own access on Keiki
You'll need a Keiki agent connected to Slack and a backend that knows your users. The identity webhook is set on a partner deployment today.
Step 1: Answer the identity webhook
Your endpoint verifies the request, looks up the Slack user, and returns their scopes. It can also return headers for the agent's MCP connections, so the person is authorized and credentialed in one round trip.
import { verifyWebhook, type IdentityWebhookAnswer, type IdentityWebhookRequest } from '@onkeiki/agents/cloud'
export async function handleIdentity(request: Request): Promise<Response> {
const body = await request.text()
if (!(await verifyWebhook(request, body, process.env.KEIKI_IDENTITY_SECRET!))) {
return new Response('invalid signature', { status: 401 })
}
const { sender } = JSON.parse(body) as IdentityWebhookRequest
const user = await findUserBySlackId(sender.teamId, sender.userId)
if (!user) return Response.json({ userId: null, scopes: [] } satisfies IdentityWebhookAnswer)
return Response.json({
userId: user.id,
role: user.role,
scopes: user.scopes, // e.g. ['billing:read'] for the intern, ['billing:read', 'billing:write'] for Maya
ttlSeconds: 300,
mcpHeaders: {
'Acme API': { Authorization: `Bearer ${await issueTokenFor(user.id)}` },
},
})
}findUserBySlackId and issueTokenFor are your own functions. Keep the mapping from Slack ids to accounts in your database, created when a person links their Slack account to your product. Don't match on display names, which anyone can change.
To keep people outside your company out entirely, return no scopes for a teamId that isn't yours. Slack Connect guests can then still ask questions and be told, by name, that they can't run a write.
Step 2: Declare the scopes each tool needs
Give every tool that reads private data or changes something a requiredScopes policy. Keiki checks the speaker's scopes before the tool runs, on any channel. A call that fails the check never reaches the tool, and the model is told who lacked which scope.
import { Effect, Schema } from 'effect'
import { defineTool } from '@onkeiki/agents'
export const refundInvoice = defineTool({
name: 'refund_invoice',
description: 'Refund one invoice. Only people with billing:write can use this.',
input: Schema.Struct({ invoiceId: Schema.String }),
policy: { requiredScopes: ['billing:write'], approval: 'ask' },
execute: (input) =>
Effect.promise(() =>
acme.refunds.create({ invoiceId: input.invoiceId }).then((refund) => ({ id: refund.id, status: refund.status })),
),
})Here acme stands in for your own API client. It runs with one service credential, so requiredScopes is the only check between a speaker and a refund. When your billing API should also enforce each person's access, serve the tool from your MCP server and call the API with that person's own credentials, as in Step 3. With approval: 'ask', a person decides before the refund runs, and Keiki checks the approver's scopes too, not just the requester's. An Approve press from someone without billing:write is refused, and the card stays pending for someone who has it. Governed actions covers approvals in detail.
A turn with no verified speaker refuses every tool with requiredScopes, so you can declare scopes before the webhook is live without opening anything up.
Step 3: Call your APIs with each person's own credentials
Scopes stop the agent from trying. Per-user credentials make sure that even a call that gets through runs with the right person's access, and your API enforces its own rules as a second line.
There are two ways to give Keiki those credentials:
- Per-sender MCP headers. Return
mcpHeadersfrom the identity webhook, as in Step 1, or set them any time withPUT /api/v1/developer/deployments/:external_ref/mcp-senders, keyed by the Slack team and user. These headers go to the MCP call, never into the prompt. The gateway never forwards headers from the chat itself. - Personal secrets. When a tool needs a person's own API key, declare it as a personal secret. The agent sends that person a single-use link that expires after 15 minutes. In a Slack thread, everyone shares one workspace folder, but each key belongs to the person who saved it, and it reaches only their sandbox commands. A Slack message that can't be attributed to a person gets no personal secrets at all.
Step 4: Revoke access when someone leaves
Answers are cached for ttlSeconds. To cut someone off sooner, call DELETE /api/v1/developer/deployments/:external_ref/identity/cache with their Slack team and user id; their next message asks your webhook again. Remove their MCP headers with the matching DELETE …/mcp-senders call. Use ttlSeconds: 0 when a revocation has to take effect on the very next message.
A checklist for any Slack agent
Whatever you build on, a few rules hold:
- Use the bot token for Slack only. Never let it stand in for a person in your own systems.
- Take the speaker from the signed Slack event, never from the message text or a tool argument the model filled in.
- Check permissions outside the model, before the tool runs, and again for whoever approves.
- Fail closed and say so. "I couldn't verify who you are" is better than a silent drop or a silent yes.
- Keep secrets out of the prompt, the transcript and the traces.
Keiki's security overview lists how the platform handles the rest: encryption at rest, tenant isolation and signed webhooks in both directions. If you're running an agent outside Slack too, the same identity model carries over; human-in-the-loop for iMessage agents shows approvals on a phone, and how to turn your API into a WhatsApp and iMessage agent shows the tool side.
FAQ
How do I set up a Slack AI bot that uses each user's own level of access?
Keep the bot token for Slack itself, and resolve every message to the person who sent it. Map their Slack user id to an account in your system, return their scopes, check those scopes before every tool call, and call your APIs with a credential issued to that person. On Keiki, the identity webhook, requiredScopes and per-sender MCP headers do these three jobs.
How can I securely connect Slack to my AI agent? Verify Slack's request signature on every event, invite the app only to the channels it needs, and keep tool credentials per user rather than one shared admin key. Check permissions outside the model, require approval for actions that change data or money, and make sure secrets never reach the prompt or the logs.
Should my Slack AI bot use a bot token or a user token? Use a bot token to read and post in Slack. Slack recommends it because it isn't tied to one person and survives that person leaving. For actions in your own systems, use credentials issued to the person speaking, so the agent can never do more than they could.
What happens when someone without permission asks the agent to do something? On Keiki, the tool call is refused before it runs, and the model is told who lacked which scope, so the agent can explain the refusal instead of failing silently.
Can prompt injection get around per-user permissions? Injection can change what the model tries to do, but not who the speaker is or which scopes they hold. Those come from the signed Slack event and your identity webhook, and they're checked before the tool runs. The worst an injected instruction can do is ask for something the speaker could already do, which is why approvals still matter for high-risk actions.
Sources
- Token types, Slack developer documentation, accessed October 2026
- Verifying requests from Slack, Slack developer documentation, accessed October 2026
- The confused deputy problem, AWS Identity and Access Management User Guide, accessed October 2026
- LLM06:2025 Excessive Agency, OWASP GenAI Security Project, Top 10 for LLM Applications 2025
- Security best practices, Model Context Protocol specification, version 2025-06-18
- Data exfiltration from Slack AI via indirect prompt injection, PromptArmor, August 20, 2024
- Slack AI can leak private data via prompt injection, The Register, August 21, 2024