Skip to content

Authentication & ACL

Rina edited this page May 12, 2026 · 1 revision

Authentication & ACL

Authorization Flow

Every incoming message passes through authorize() in auth.ts:

Message received
    │
    ├─ Is author a bot? → SKIP
    ├─ Is it a DM? → SKIP
    ├─ Is channel registered (created via /new)? → ALLOW (no ACL check needed)
    ├─ Is user ID in ACL? → ALLOW
    ├─ Does user have an ACL-listed role? → ALLOW
    ├─ Is channel ID in ACL? → ALLOW
    └─ Otherwise → SKIP

Key Points

  • Registered channels (created via /new) are unconditionally trusted — all messages pass through
  • ACL only applies to non-registered channels
  • Authorization is synchronous — reads from in-memory Sets, no database hit per message
  • The check runs on every MessageCreate event

Bot Owners

Bot owners are auto-detected from two sources:

  1. Discord Application Info — fetched via GET /oauth2/applications/@me (the application owner/team members)
  2. BOT_OWNER_IDS env var — comma-separated Discord user IDs

Results are cached for 60 seconds. Bot owners can use /acl commands.

ACL System

Data Model

The AclState class is an in-memory write-through cache:

  • Reads: Always from in-memory Set<string> (per type: users, roles, channels)
  • Writes: Go to Prisma DB first, then update the in-memory Set

Entry Types

Type Value What it allows
user Discord user ID Messages from this user in any non-registered channel
role Discord role ID Messages from any user with this role
channel Discord channel ID All messages in this specific channel

Commands

/acl add type:user value:123456789012345678
/acl add type:role value:@SomeRole
/acl add type:channel value:#some-channel
/acl remove type:user value:123456789012345678
/acl list

Database Schema

model AclEntry {
  id      Int      @id @default(autoincrement())
  type    String   // "user", "role", "channel"
  value   String   // Discord snowflake ID
  addedBy String   @map("added_by")
  addedAt DateTime @default(now()) @map("added_at")

  @@unique([type, value])
  @@map("acl_entries")
}

Permission Modes

Each room has a mode that controls Claude Code's permission level:

Mode Flag Description
default (none) Standard Claude permissions — prompts for file edits, commands, etc.
plan --permission-mode plan Claude can only plan, not execute
acceptEdits --permission-mode acceptEdits Claude can edit files without prompting
bypassPermissions --dangerously-skip-permissions All permission checks disabled

Note: bypassPermissions is the default mode for new rooms. Change with /new name:foo mode:default or /mode mode:default.

Changing the mode via /mode kills the current tmux session — the next /enter restarts Claude with updated flags.

Clone this wiki locally