The /help command
/help answers one question: what can I run here?
It is built per member, from the server's real command set, filtered to what that member's permissions actually allow, and replied ephemerally. A moderator and an ordinary member asking the same question get different answers, and neither is shown something they would be refused.
Every server has it
/help is in ALWAYS_ENABLED_BUILT_INS (src/lib/db/commands.ts). A guild can
change its permission gate like any other built-in, but it cannot turn it off:
setBuiltInCommandOverriderefusesenabled: falsefor it, so the dashboard can say why rather than showing a toggle that springs back.applyBuiltInOverrideforcesenabledtrue when reading, so an override written before this rule existed has no effect.- The dashboard shows an Always on badge in place of the toggle.
"Always available" also means registered. A guild's command set only ever
reached Discord on a command write, so a freshly invited bot had no commands at
all until an admin saved one. Events.GuildCreate now calls
POST /api/guilds/{guildId}/sync-commands — bot-token authenticated, like the
room lobby — so joining a server registers its commands immediately. That
handler ignores the GuildCreate burst that arrives while the gateway is
starting up, which is not a join; those all land before the client is ready.
What gets listed
collectGuildCommands (src/lib/discord/commands.ts) is the single source for
both /help and syncGuildCommands. Registering and listing read the same
set on purpose — the alternative is a command that exists in Discord and not in
the help, or the reverse.
| Source | Where it comes from |
|---|---|
built-in |
commands rows under __built_in__, guild overrides applied |
server |
The guild's own enabled commands |
integration |
Commands from the guild's enabled integrations |
A command family is listed with its subcommands underneath, so /room shows
/room create, /room lock and the rest rather than one opaque line.
Filtering
memberMayUseCommand decides (the permission level plus role rules, see
Command access), the same function the interaction
dispatcher uses to gate the command itself. That matters: Discord's own
default_member_permissions gate is advisory, and this is the one that decides
whether the command would actually run.
It fails closed. A restricted command whose caller's permissions cannot be read is left out — a DM has no member, and guessing "allowed" would advertise moderator commands to everyone.
Filtering stops at the command gate. /room has role lists of its own in its
preset, and a member whose roles that preset denies still sees /room here;
the command is available to them, and it explains itself when they run it.
Limits
Discord caps an embed at 25 fields and a field value at 1024 characters. A server with more commands than that gets the overflow named in a final field rather than silently cut — a member who cannot see a command in help concludes it does not exist.
In DMs
A DM has no guild and no member, so /help lists the built-ins whose
dm_permission lets Discord deliver them there.
Where the code is
| Piece | File |
|---|---|
| Listing and embed | src/lib/discord/help.ts |
| Shared command set | collectGuildCommands in src/lib/discord/commands.ts |
| Always-on rule | ALWAYS_ENABLED_BUILT_INS in src/lib/db/commands.ts |
| Interaction handling | src/routes/api/discord/interactions/+server.ts |
| Join registration | src/routes/api/guilds/[guildId]/sync-commands/, Events.GuildCreate in src/lib/discord/gateway.ts |
| Stored row | migrations/0066_help_command_dynamic.sql |