arizuko › products › Slack team agent — setup
Slack team agent — setup
Deploy one agent into a Slack channel: arizuko instance, Slack App, OAuth-linked identities, optional per-teammate web chat.
prerequisites
- A host with Docker, ports 80/443 reachable behind a TLS terminator (or run with
WEB_HOSTon a reverse proxy that handles certs). - A Slack workspace where you can install a Slack App.
- The
arizukobinary and thearizuko-antagent image built locally (make build,sudo make images,sudo make agent). - Anthropic API access for the in-container agent.
1. seed the instance
arizuko create acme --product slack-team
cd /srv/data/arizuko_acme
$EDITOR .env
arizuko create --product slack-team seeds the data
dir at /srv/data/arizuko_acme/ with the slack-team
product applied: main group gets the slack-team
PERSONA.md, CLAUDE.md overlay, and a
starter facts/ dir. Plus the default
.env and the web/ tree copied from
template/web/. Without --product the
instance comes up with the generic agent persona; the rest of
this guide assumes you used the product flag.
2. fill in .env
The Slack-team product needs at least these keys set:
# Identity + secrets
ASSISTANT_NAME=acme # agent display name
AUTH_SECRET=… # JWT signer; arizuko create pre-generates it
WEB_HOST=acme.example.com # public hostname for proxyd / OAuth callbacks
# Slack adapter
SLACK_BOT_TOKEN=xoxb-… # from the Slack App "OAuth & Permissions" page
SLACK_SIGNING_SECRET=… # from the Slack App "Basic Information" page
# OAuth providers for linked identities (any subset)
GITHUB_CLIENT_ID=…
GITHUB_CLIENT_SECRET=…
GOOGLE_CLIENT_ID=…
GOOGLE_CLIENT_SECRET=…
DISCORD_CLIENT_ID=…
DISCORD_CLIENT_SECRET=…
arizuko create already generated AUTH_SECRET
and SECRETS_KEY — leave those alone.
See reference / env for the
complete list and the daemons that read each one.
3. create the Slack App
In api.slack.com/apps, create a new app (from scratch) in your workspace, then:
- OAuth & Permissions. Add bot scopes
channels:history,channels:read,groups:history,groups:read,im:history,im:read,mpim:history,mpim:read,chat:write,chat:write.public,reactions:read,reactions:write,files:read,files:write,users:read,assistant:write(required for the AI sidebar pane). Install to your workspace and copy the Bot User OAuth Token intoSLACK_BOT_TOKEN. - Event Subscriptions. Enable, set the request URL to
https://<WEB_HOST>/slack/events, subscribe to bot eventsmessage.channels,message.groups,message.im,message.mpim,reaction_added,member_joined_channel. Do not subscribe toapp_mention— slakd derivesverb=mentionfrom message text and the duplicate event would double-fire. - Basic Information. Copy the Signing Secret into
SLACK_SIGNING_SECRET.
The /slack/ path is wired into proxyd by
template/services/slakd.toml — nothing to edit in
compose generation. Slack's request signature is verified by
slakd using the signing secret.
enable the AI sidebar (optional)
The AI sidebar is the per-user pane behind Slack’s AI
icon — suggested-prompt buttons, a per-pane title, and
full channel context. It requires the assistant:write
scope (already in the list above). In the Slack App →
Agents & AI Apps, toggle “Agent or
Assistant” on. Step-by-step with screenshots:
Slack adapter how-to,
step 8. Verify by clicking the AI icon next to the bot in
Slack — a pane should open.
The agent steers the pane at runtime via the
pane_set_title and pane_set_prompts
MCP tools — title and suggested prompts can change per turn,
per channel, per context. Branding is the env var
ARIZUKO_ASSISTANT_NAME (instance-wide). No
static per-folder frontmatter; runtime steering + instance
env covers both axes.
4. run the instance
arizuko run acme
This generates a Docker Compose file in
/srv/data/arizuko_acme/compose.yml and runs it.
For long-running production, install a systemd unit; see
getting started for the template.
5. invite teammates
Plain channel use needs no sign-in. The route rule that maps
the Slack channel to an arizuko group is the auth fence:
anyone the workspace lets into the channel can talk to the
bot, and per-channel grants gate what the agent will do.
OAuth login is only needed for surfaces that key off a
cross-platform user identity — the private web chat,
per-user secrets (spec 7/Y),
and merging memory across Slack + GitHub + Discord. To opt
in, a teammate signs in at
https://<WEB_HOST>/auth/login; subsequent
logins on any linked provider resolve to the same
auth_users row.
To pre-approve teammates, issue an invite token scoped to the folders it admits into (a folder glob, not a user pattern):
arizuko invite acme create 'main/**' --max-uses 20 --expires 336h
arizuko invite acme list
Or open access via onboarding: set
ONBOARDING_ENABLED=true and
ONBOARDING_PLATFORMS=slack in .env.
Teammates who DM the bot land in the admission queue; the
operator approves from /dash/.
6. verify
sudo systemctl status arizuko_acme(ordocker compose ps) shows all services up.curl -s https://<WEB_HOST>/slack/healthreturns200. A503 disconnectedmeans slakd is up but the Slack token is rejected — double-check the bot token and that the app is installed in the workspace.- Invite the bot to a channel (
/invite @acme), then send@acme hello. The agent replies in-thread. - Visit
https://<WEB_HOST>/dash/as the operator (root) — you should see the channel as a group with a route.
7. recommended routing default
For a workspace bot, the usual shape is “answer every DM,
stay quiet in channels and mpims unless @-mentioned.” In
the root agent (or via /dash/ → Routing):
seq match target
-2 chat_jid=slack:*/dm/* main
-1 chat_jid=slack:*/channel/* verb=mention main
-1 chat_jid=slack:*/group/* verb=mention main
0 chat_jid=slack:*/channel/* main#observe
0 chat_jid=slack:*/group/* main#observe
Channel chatter still lands in main under
#observe so the agent has context on the next
mention, but no turn fires until someone calls on it. Full
recipe and Discord variant:
asymmetric channels.
8. tune per channel
Each Slack channel maps to an arizuko group folder. To
customise voice, allowed tools, or skills for one channel,
edit its ~/CLAUDE.md — either via WebDAV at
https://<WEB_HOST>/dav/<channel>/CLAUDE.md
or directly on disk under
/srv/data/arizuko_acme/groups/<channel>/.
For per-teammate scope (memory file, grants), the agent
writes to the channel's users/ subfolder
automatically — you do not seed it. Skill toggles and
grant rules are managed from /dash/ or via the
arizuko group grant CLI.
secrets table — set with
arizuko secret <instance> set <folder> KEY --value V.
They merge into the agent container's env at spawn time;
nothing leaks to other channels.
9. adding more channels
Each Slack channel becomes its own arizuko group folder. Map a new channel ID to a folder:
arizuko group acme add 'slack:T012ABCD/channel/C0HJKL456' eng-support
The JID matches the route table (see step 7). For a
per-channel persona or skill set, edit
~/CLAUDE.md in the new folder — over
WebDAV at
https://<WEB_HOST>/dav/eng-support/CLAUDE.md
or directly under
/srv/data/arizuko_acme/groups/eng-support/.
Per-channel grants — who can run which actions in this
folder — live in the acl table. Operator
grants are admin-scoped via
arizuko group acme grant <sub> '<pattern>';
finer per-role and per-user rules
(interact, mcp:<tool>, deny
effects) are managed from /dash/ → Grants
today. Full rule shape:
grants.
CLI reference: cli.
autoviv (optional)
If you don’t want to run arizuko group add
for every new channel, set a catch-all route to a tier-1
agent (e.g. atlas) and add an autoviv paragraph
to that agent’s CLAUDE.md. New channels
the bot is invited to land in atlas on first
message; the agent calls register_group itself
and the next message in that channel routes to its own
folder. Recipe and limits:
autoviv.
10. multi-workspace
One slakd process talks to one workspace —
the bot token is read once at startup
(slakd/main.go calls
chanlib.MustEnv("SLACK_BOT_TOKEN")). Two ways
to cover more than one workspace.
option A — one arizuko instance, multiple slakd
Drop a second service TOML beside the default
slakd.toml. The compose generator picks up every
services/*.toml file
(compose/compose.go reads services/
in lexical order) and names each container
arizuko_<file>_<flavor>, so
slakd-acme.toml and slakd-beta.toml
run side by side. This is the shipped pattern in
specs/5/R-multi-account.md.
Files named <adapter>-<label>.toml
share the base adapter's env_file (see
envFileFor) — per-workspace tokens go in
the TOML's [environment] block, not in
.env.
# /srv/data/arizuko_acme/services/slakd-beta.toml
image = "arizuko:latest"
entrypoint = ["slakd"]
volumes = ["${HOST_DATA_DIR}:${CONTAINER_DATA}"]
[environment]
ROUTER_URL = "http://routd:8080"
SLACK_BOT_TOKEN = "xoxb-beta-workspace-token"
SLACK_SIGNING_SECRET = "beta-signing-secret"
ASSISTANT_NAME = "${ASSISTANT_NAME}"
LISTEN_ADDR = ":8080"
LISTEN_URL = "http://slakd-beta:8080"
[[proxyd_route]]
path = "/slack-beta/"
backend = "http://slakd-beta:8080"
auth = "public"
gated_by = "SLACK_BOT_TOKEN"
preserve_headers = ["X-Slack-Signature", "X-Slack-Request-Timestamp"]
Set the second Slack App's Event Subscription URL to
https://<WEB_HOST>/slack-beta/events —
distinct path keeps the two webhooks separate. Each
workspace's JIDs already carry its team ID
(slack:T012/channel/C0HJK, see
slakd/jid.go), so route rules and group
mappings stay per-workspace without extra plumbing.
arizuko run acme then brings both containers
up; restart the instance after dropping the file.
Pick this when the workspaces belong to the same operator
and should share state — one Anthropic key, one
/dash/, one /auth/ OAuth setup,
one store/ of databases, one set of users that
can sign in across workspaces. The cost is shared spend and
shared blast radius: a bad skill or an Anthropic outage
hits every workspace at once.
option B — one arizuko instance per workspace
Run arizuko create acme and
arizuko create beta as separate instances on
the same host. Each gets its own WEB_HOST,
its own database, its own systemd unit, its own Anthropic
key, its own /dash/. Pick this for
per-customer isolation — one workspace's runaway
agent or compromised token can't reach the other.
11. webhook ingest (optional)
Bring external events — GitHub PRs, Linear issues,
Sentry alerts — into a Slack-team agent by minting a
hook token for the
folder. Events arrive as messages at
hook:<folder>/<source>; the body is
delivered verbatim. Full recipe:
webhooks.
Quick GitHub example. Mint a hook token for the engineering folder, add a route for its JID, and paste the URL into the repo's webhook config:
# mint via CLI (operator) or issue_webhook (agent)
arizuko token acme issue webhook main/eng github
# webhook URL: https://<WEB_HOST>/hook/<token>
arizuko route acme add 'chat_jid=hook:main/eng/github' main/eng --seq -5
# in github.com/<repo>/settings/hooks:
# Payload URL: https://<WEB_HOST>/hook/<token>
# Content-Type: application/json
Events arrive in the main/eng folder with
chat_jid = hook:main/eng/github and the request
body delivered verbatim. Route from there to your
main Slack channel with a normal route rule, or let the
main/eng agent triage and
escalate_group to its parent when something
needs human attention. Cross-channel and cross-platform
moves use the same MCP tools the agents already share
(talking to agents).
Parsing the payload and (optional) X-Hub-Signature-256
verification are skill concerns on the agent side —
the platform is intentionally hands-off. The URL token is
the bearer credential; rotate by reissue. See the
webhooks security model.
email ingest
For email-as-webhook (forwarded alerts, mailing-list
digests), use the emaid adapter instead of a
hook token — it owns IMAP receive, SMTP send, and
threading by Message-ID. See
emaid for the
connection setup. Auth-related env vars (spec 10/17) gate
whether inbound is treated as trusted:
# In .env — required to mark ANY inbound as trusted
EMAIL_TRUSTED_AUTHSERV=mx.google.com # your upstream MX authserv-id
EMAIL_TRUSTED_DOMAINS=mycompany.com,partner.com # optional; empty = DMARC alone gates
EMAIL_STRICT_AUTH=false # true = drop unauthenticated outright
Without EMAIL_TRUSTED_AUTHSERV set, every inbound
is shipped with verb=untrusted and the slack-team
CLAUDE.md rule routes it to quarantine. Set
EMAIL_TRUSTED_AUTHSERV to your provider's
authserv-id (Gmail: mx.google.com, Fastmail:
mail.fastmail.com) to enable DMARC-pass trust.
Full design: specs/10/17-emaid-auth.md.
12. compose with support (optional)
The slack-team product is a generalist agent — it answers
questions, summarizes threads, files issues. For a more
focused experience in one particular channel (say
#eng-support), compose the
support product as a child group
under the slack-team root. The result: most channels keep the
slack-team persona; #eng-support gets the
support persona — KB-first, cite-or-say-I-don't-know,
escalate-when-stuck.
why compose, don't replace
Both products ship the same default skillset (diary, facts,
recall-memories, users, issues, web). They differ in
PERSONA.md + CLAUDE.md only. By
running them as sibling groups under one Slack instance you
get one bot identity in Slack (operator manages one app,
one token, one onboarding flow) but two distinct behaviors,
scoped by channel.
setup
With slack-team already deployed (sections 1–6 above done), add a support sub-group:
# 1. create the sub-group with the support product seed
arizuko group acme add 'slack:T123/channel/C456' main/support --product support
# 2. populate facts/ — copy your KB markdown into
# /srv/data/arizuko_acme/groups/main/support/facts/
# (one file per topic; the support persona's first move is /recall-memories)
$EDITOR /srv/data/arizuko_acme/groups/main/support/facts/<topic>.md
# 3. (optional) override the auto-seeded route to fire on mention only,
# not on every msg. add_route takes ONE 'route' arg with the rule as
# JSON (seq lower = higher priority; the bare 'add' above writes
# seq=0; -3 wins over it).
mcpc @s tools-call add_route \
route:='"{\"seq\":-3,\"match\":\"chat_jid=slack:T123/channel/C456 verb=mention\",\"target\":\"main/support\"}"'
mcpc @s tools-call add_route \
route:='"{\"seq\":-2,\"match\":\"chat_jid=slack:T123/channel/C456\",\"target\":\"main/support#observe\"}"'
After add_route calls the agent has full per-channel
scope: messages from C456 hit
main/support's session, with its persona, its
memory, its facts/ dir — isolated from the
rest of slack-team. Threading state is per-group so
cross-channel context doesn't leak.
what each piece gives you
-
slack-team (root,
main): generalist voice, answers any channel without a more specific route. Persona atgroups/main/PERSONA.md. -
support (child,
main/support): fact-cite voice, refuses to guess, escalates to the parent group when stuck. Persona atgroups/main/support/PERSONA.md. Behavior rules (KB lookup order, escalation procedure, citation format) atgroups/main/support/CLAUDE.md. -
Routes are the auth fence: a Slack user
who can post in
#eng-supporttalks to the support persona; the same user in#generaltalks to the slack-team persona. Grants onmain/supportcan further constrain what the support agent will do per-user. -
WebDAV: each group's
facts/,PERSONA.md,CLAUDE.mdare editable at/dav/main/and/dav/main/support/. Operators tune persona per group in Finder / VS Code without docker exec.
composing more products
Same pattern, different child folder. Add main/eng
with --product pm for engineering-channel
project-management voice; main/marketing with
--product creator; etc. Each child is one
arizuko group add + one route. The slack-team
parent stays generalist; specialized children inherit nothing
from it (group-level isolation by design).
Limits: each child group runs its own container per agent turn, so
the global concurrency cap matters
(MAX_CONCURRENT_CONTAINERS, default 5). The separate
max_children setting at
/dash/groups/main/settings caps how many child groups
the agent may register itself (0 = disabled, the default;
host-CLI group add bypasses it).
tradeoff: when to NOT compose
Small teams with one busy channel: the slack-team persona is
already capable. Composing support adds a folder, a route, a
facts/ to maintain — only worth it when you can name the
channel that needs different behavior. Start with slack-team
flat; promote a channel to its own sub-group when you find
yourself rewriting main/CLAUDE.md for one
channel's quirks.
common issues
- Bot dropped from the channel.
- Slack removes the bot when a channel is archived or an
admin kicks it. Re-invite with
/invite @acmefrom inside the channel. No restart needed. - Bot is in the channel but doesn’t respond.
- Check two things: (1) the route table matches this
channel’s JID and target is not
#observefor mentions (see step 7); (2) the calling user has at leastinteractin this folder — check/dash/→ Grants. - Slack token rejected /
/slack/healthreturns 503. - Token was rotated or the app was reinstalled. Copy the
new Bot User OAuth Token into
SLACK_BOT_TOKENin.envand restart:sudo systemctl restart arizuko_acme. - AI sidebar feels sluggish.
- Slack’s
assistant.threads.*endpoints are Tier 2 (rate-limited). Grepsudo journalctl -u arizuko_acme | grep 429— if you see throttle warnings, the agent is callingpane_set_status/pane_set_promptsmore often than Slack allows for that workspace. - Per-user memory not updating after OAuth login.
- Linkage didn’t complete — visit
https://<WEB_HOST>/auth/loginas the user and sign in again with the provider they want linked. Check/dash/→ Users to see the resolvedauth_usersrow and its linked identities.
go deeper
- Slack team agent — intro — what teammates see
- Slack adapter how-to — deeper notes on Slack-specific quirks
- grants — per-user and per-folder action scoping
- auth — OAuth providers, account linking
- slack pane — AI sidebar surface, pane events, suggested prompts
- personas — how voice + skills + frontmatter shape a channel agent
- slakd — the Slack adapter daemon
- CLI reference — full command list