arizuko › Slack team agent › setup
Deploy one agent into a Slack channel: arizuko instance, Slack App, OAuth-linked identities, optional per-teammate web chat.
WEB_HOST on a reverse proxy that handles certs).arizuko binary and the arizuko-ant agent image built locally (make build, sudo make images, sudo make agent).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.
.envThe Slack-team product needs at least these keys set:
# Identity + secrets
ASSISTANT_NAME=acme # agent display name
CHANNEL_SECRET=<random hex> # signing between adapters and gated
AUTH_SECRET=<random hex> # JWT signer for /auth/, /dash/, /chat/
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=…
Generate the two secrets with openssl rand -hex 32.
See reference / env for the
complete list and the daemons that read each one.
In api.slack.com/apps, create a new app (from scratch) in your workspace, then:
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 into SLACK_BOT_TOKEN.https://<WEB_HOST>/slack/events, subscribe to bot events message.channels, message.groups, message.im, message.mpim, reaction_added, member_joined_channel. Do not subscribe to app_mention — slakd derives verb=mention from message text and the duplicate event would double-fire.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.
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.
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.
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.
Invite their primary Slack/web identity into the channel group
with the invite CLI:
arizuko invite acme create 'slack:user/U*' --max-uses 20 --expires 14d
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/.
sudo systemctl status arizuko_acme (or docker compose ps) shows all services up.curl -s https://<WEB_HOST>/slack/health returns 200. A 503 disconnected means slakd is up but the Slack token is rejected — double-check the bot token and that the app is installed in the workspace./invite @acme), then send @acme hello. The agent replies in-thread.https://<WEB_HOST>/dash/ as the operator (root) — you should see the channel as a group with a route.
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.
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.
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.
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.
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.
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://gated:8080"
SLACK_BOT_TOKEN = "xoxb-beta-workspace-token"
SLACK_SIGNING_SECRET = "beta-signing-secret"
CHANNEL_SECRET = "${CHANNEL_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 messages.db, 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.
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.
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 and paste the URL into the repo's webhook config:
# mint via CLI (operator) or issue_webhook (agent)
arizuko token issue atlas/eng hook github
# https://<WEB_HOST>/hook/<token>
# in github.com/<repo>/settings/hooks:
# Payload URL: https://<WEB_HOST>/hook/<token>
# Content-Type: application/json
Events arrive in the atlas/eng folder with
chat_jid = hook:atlas/eng/github and the full
request body and headers preserved. Route from there to your
main Slack channel with a normal route rule, or let the
atlas/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).
webd delivers GitHub's JSON verbatim with the
X-GitHub-Event and signature headers attached.
Parsing 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.
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.
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.
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.
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/.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.
main):
generalist voice, answers any channel without a more specific
route. Persona at
groups/main/PERSONA.md.
main/support):
fact-cite voice, refuses to guess, escalates to the parent
group when stuck. Persona at
groups/main/support/PERSONA.md. Behavior rules
(KB lookup order, escalation procedure, citation format)
at groups/main/support/CLAUDE.md.
#eng-support talks to the
support persona; the same user in #general
talks to the slack-team persona. Grants on
main/support can further constrain what the
support agent will do per-user.
facts/,
PERSONA.md, CLAUDE.md are
editable at /dav/main/ and
/dav/main/support/. Operators tune persona
per group in Finder / VS Code without docker exec.
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 runs its own container per agent turn, so
spawn budget matters. Tune via the parent's
max_children (default 16 from v0.40.1
onward) at /dash/groups/main/settings.
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.
/invite @acme
from inside the channel. No restart needed.#observe
for mentions (see step 7); (2) the calling user has at least
interact in this folder — check
/dash/ → Grants./slack/health returns 503.SLACK_BOT_TOKEN
in .env and restart:
sudo systemctl restart arizuko_acme.assistant.threads.* endpoints
are Tier 2 (rate-limited). Grep
sudo journalctl -u arizuko_acme | grep 429 —
if you see throttle warnings, the agent is calling
pane_set_status / pane_set_prompts
more often than Slack allows for that workspace.https://<WEB_HOST>/auth/login as the user
and sign in again with the provider they want linked.
Check /dash/ → Users to see the resolved
auth_users row and its linked identities.