Criar um Discord Bot com Claude Code: Slash Commands com discord.js
Crie um Discord Bot seguro com Claude Code, discord.js e três Slash Commands úteis para suporte.
Você abre o canal de suporte e encontra três mensagens: “não funciona”, “alguém pode ajudar?” e um @everyone disparado por engano. O problema não é falta de vontade da equipe. Falta um caminho que peça as informações certas sem dar poder demais ao bot.
Neste guia, vamos usar Claude Code e discord.js para criar um Discord Bot com três Slash Commands: /support, /faq e /handoff. O exemplo foi pensado para quem ainda não publicou um bot. Ele roda com Node 24 LTS, fixa as dependências, responde primeiro de forma privada e impede que o texto do usuário acione mentions.
O que você vai obter:
- um projeto copiável com
discord.js14.27.0 edotenv17.2.3; - registro de comandos apenas no servidor de teste;
deferReply()seguido deeditReply()para evitar respostas expiradas;- permissões mínimas e proteção do token;
- uma verificação local que não se conecta ao Discord.
O que Claude Code faz e o que continua sendo decisão humana
Claude Code é útil para criar arquivos, revisar diferenças, executar verificações e apontar permissões excessivas. Ele não conhece a política real da sua comunidade nem deve decidir sozinho quem pode acessar dados de suporte.
| Claude Code pode preparar | Uma pessoa precisa decidir |
|---|---|
| Estrutura do projeto e Slash Commands | Quem pode usar /handoff |
| Validação de variáveis e testes locais | Qual canal recebe dados privados |
| Sanitização de mentions e tratamento de erros | Quanto tempo guardar mensagens e logs |
| Checklist de deploy e rollback | Quando promover comandos para produção |
Peça mudanças pequenas e revisáveis. Um bom primeiro pedido é: “Crie um bot de suporte com /support, /faq e /handoff. Use Node 24 LTS, discord.js 14.27.0, respostas privadas, permissões mínimas e um teste local sem conexão externa.”
As regras oficiais de comandos estão em Discord Application Commands. O prazo e as formas de resposta ficam em Receiving and Responding to Interactions. Para conferir classes e métodos desta versão, use a API do discord.js 14.27.0.
Antes e depois da implantação
| Antes | Depois |
|---|---|
| Pedido solto no canal geral | /support exige resumo, gravidade e contexto |
| Respostas repetidas para dúvidas comuns | /faq devolve uma orientação curta e privada |
| Troca de moderador sem contexto | /handoff registra alvo, autor e próxima ação |
| Texto do usuário pode gerar notificações | allowedMentions e sanitização bloqueiam mentions |
O bot organiza a entrada; ele não substitui o atendimento. Uma pessoa ainda confirma prioridade, interpreta o contexto e escolhe a ação seguinte.
Monte o projeto com versões fixas
Este artigo assume Node 24 LTS. O campo engines aceita Node 18 ou superior para deixar a exigência do projeto explícita; valide sua plataforma antes do deploy.
mkdir discord-support-bot
cd discord-support-bot
npm init -y
mkdir src
Substitua o package.json por:
{
"name": "discord-support-bot",
"private": true,
"type": "module",
"engines": {
"node": ">=18"
},
"scripts": {
"start": "node src/bot.js",
"verify": "node --check src/bot.js && node src/verify.js"
},
"dependencies": {
"discord.js": "14.27.0",
"dotenv": "17.2.3"
}
}
Instale exatamente o que está declarado e preserve o package-lock.json:
npm install
Crie .gitignore:
node_modules/
.env
Crie .env apenas na sua máquina:
DISCORD_TOKEN=cole_o_token_do_bot
DISCORD_CLIENT_ID=cole_o_application_id
DISCORD_GUILD_ID=cole_o_id_do_servidor_de_teste
SUPPORT_CHANNEL_ID=cole_o_id_do_canal_de_suporte
DEPLOY_COMMANDS=true
O DISCORD_TOKEN é uma senha. Não cole o valor em prompts, commits, prints ou logs. Se ele aparecer em algum desses lugares, gere outro token no Developer Portal antes de continuar.
Código dos comandos /support, /faq e /handoff
Crie src/safety.js. Esta função reduz o risco de transformar texto enviado por usuários em notificações para todo o servidor.
export function neutralizeMentions(value) {
return value
.replaceAll("@everyone", "@ everyone")
.replaceAll("@here", "@ here")
.replace(/<@!?(\d+)>/g, "user:$1")
.replace(/<@&(\d+)>/g, "role:$1");
}
Agora crie src/bot.js:
import "dotenv/config";
import {
Client,
Events,
GatewayIntentBits,
MessageFlags,
PermissionFlagsBits,
REST,
Routes,
SlashCommandBuilder,
} from "discord.js";
import { neutralizeMentions } from "./safety.js";
const env = {
token: process.env.DISCORD_TOKEN,
clientId: process.env.DISCORD_CLIENT_ID,
guildId: process.env.DISCORD_GUILD_ID,
supportChannelId: process.env.SUPPORT_CHANNEL_ID,
};
for (const [name, value] of Object.entries(env)) {
if (!value) throw new Error(`Missing environment variable: ${name}`);
}
const commands = [
new SlashCommandBuilder()
.setName("support")
.setDescription("Send a structured support request")
.addStringOption((option) =>
option.setName("summary").setDescription("What happened?").setMaxLength(900).setRequired(true),
)
.addStringOption((option) =>
option
.setName("severity")
.setDescription("How urgent is it?")
.setRequired(true)
.addChoices(
{ name: "low", value: "low" },
{ name: "normal", value: "normal" },
{ name: "high", value: "high" },
),
)
.addStringOption((option) =>
option.setName("context").setDescription("Steps or error text").setMaxLength(1500),
),
new SlashCommandBuilder()
.setName("faq")
.setDescription("Show one short answer")
.addStringOption((option) =>
option
.setName("topic")
.setDescription("Choose a topic")
.setRequired(true)
.addChoices(
{ name: "setup", value: "setup" },
{ name: "permissions", value: "permissions" },
{ name: "rollout", value: "rollout" },
),
),
new SlashCommandBuilder()
.setName("handoff")
.setDescription("Create a moderator handoff note")
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
.addUserOption((option) =>
option.setName("target").setDescription("User related to the case").setRequired(true),
)
.addStringOption((option) =>
option.setName("note").setDescription("What should happen next?").setMaxLength(1500).setRequired(true),
),
].map((command) => command.toJSON());
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
async function supportChannel() {
const channel = await client.channels.fetch(env.supportChannelId);
if (!channel?.isTextBased() || typeof channel.send !== "function") {
throw new Error("SUPPORT_CHANNEL_ID must point to a writable text channel");
}
return channel;
}
async function handleSupport(interaction) {
const summary = interaction.options.getString("summary", true);
const severity = interaction.options.getString("severity", true);
const context = interaction.options.getString("context") ?? "No extra context";
const channel = await supportChannel();
await channel.send({
content: [
"**New support request**",
`Reporter: ${interaction.user.tag} (${interaction.user.id})`,
`Severity: ${severity}`,
`Summary: ${neutralizeMentions(summary)}`,
`Context: ${neutralizeMentions(context)}`,
].join("\n"),
allowedMentions: { parse: [], repliedUser: false },
});
await interaction.editReply("Your request was sent to the support team.");
}
async function handleFaq(interaction) {
const answers = {
setup: "Use Node 24 LTS, configure .env, then test guild commands.",
permissions: "Start with View Channel and Send Messages; do not grant Administrator.",
rollout: "Test in one guild before registering global commands.",
};
const topic = interaction.options.getString("topic", true);
await interaction.editReply(answers[topic] ?? "No answer is configured for this topic.");
}
async function handleHandoff(interaction) {
if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageMessages)) {
await interaction.editReply("Manage Messages permission is required.");
return;
}
const target = interaction.options.getUser("target", true);
const note = interaction.options.getString("note", true);
const channel = await supportChannel();
await channel.send({
content: [
"**Moderator handoff**",
`Target: ${target.tag} (${target.id})`,
`From: ${interaction.user.tag} (${interaction.user.id})`,
`Next action: ${neutralizeMentions(note)}`,
].join("\n"),
allowedMentions: { parse: [], repliedUser: false },
});
await interaction.editReply("The handoff note was created.");
}
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (!interaction.inGuild()) {
await interaction.reply({ content: "Use this command inside the server.", flags: MessageFlags.Ephemeral });
return;
}
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
try {
if (interaction.commandName === "support") await handleSupport(interaction);
else if (interaction.commandName === "faq") await handleFaq(interaction);
else if (interaction.commandName === "handoff") await handleHandoff(interaction);
else await interaction.editReply("Unknown command.");
} catch (error) {
console.error("Interaction failed:", error instanceof Error ? error.message : error);
await interaction.editReply("The command failed. Ask a moderator to check the logs.");
}
});
async function deployGuildCommands() {
const rest = new REST({ version: "10" }).setToken(env.token);
await rest.put(Routes.applicationGuildCommands(env.clientId, env.guildId), { body: commands });
}
if (process.env.DEPLOY_COMMANDS === "true") await deployGuildCommands();
client.once(Events.ClientReady, (readyClient) => console.log(`Ready as ${readyClient.user.tag}`));
await client.login(env.token);
Por que responder com deferReply() antes? Uma chamada ao canal ou a outro serviço pode demorar. O defer confirma a interaction e reserva uma resposta privada; editReply() entrega o resultado quando o trabalho termina. Isso também deixa o fluxo de erro previsível.
No Developer Portal, use os scopes bot e applications.commands. Para este código, comece apenas com View Channels e Send Messages no canal de suporte. /handoff ainda verifica Manage Messages para a pessoa que executa o comando; isso não significa que o bot precise de Administrator.
Três Use cases com responsabilidade clara
1. Entrada de suporte
Entrada: /support summary:"falha no npm install" severity:normal context:"erro EACCES".
Saída do bot: uma mensagem privada de confirmação e uma ficha estruturada no canal de suporte, sem disparar mentions.
Decisão humana: reproduzir o erro, pedir logs sem secrets e definir prioridade. O bot não decide se o incidente é crítico.
2. FAQ curta
Entrada: /faq topic:permissions.
Saída do bot: uma resposta privada lembrando que Administrator não é o ponto de partida.
Decisão humana: atualizar a orientação quando a política da equipe mudar. A FAQ não deve virar uma cópia desatualizada da documentação.
3. Handoff entre moderadores
Entrada: /handoff target:@usuario note:"confirmar versão do Node e revisar o log sanitizado".
Saída do bot: uma nota no canal interno com alvo, autor e próxima ação. O texto não gera ping.
Decisão humana: escolher o novo responsável, limitar o acesso ao canal e excluir dados quando o prazo de retenção terminar.
Pitfalls que quebram um bot aparentemente simples
Responder tarde demais. Fazer chamadas externas antes de responder pode invalidar a interaction. Use deferReply() no início e editReply() no final.
Usar uma versão flutuante. Uma atualização inesperada pode mudar a API no próximo deploy. Fixe versões e revise o package-lock.json como qualquer outro código.
Registrar comandos globais durante o desenvolvimento. O exemplo usa applicationGuildCommands, limitado ao servidor de teste. Só publique globalmente depois de revisar nomes, permissões e rollback.
Confiar apenas na troca visual de @everyone. A sanitização ajuda a leitura, mas a barreira efetiva está também em allowedMentions: { parse: [] }. Mantenha as duas.
Vazar o token. .gitignore não apaga um segredo que já entrou no histórico. Ao detectar exposição, rotacione o token e revise logs, CI e mensagens enviadas a ferramentas de IA.
ROI simples, com hipóteses visíveis
Este cálculo não é um resultado medido. Suponha 30 pedidos por dia, 20 dias por mês e economia de 45 segundos na triagem de cada pedido. Isso dá 30 x 20 x 45 / 3600 = 7,5 horas por mês. Com custo interno hipotético de R$ 60 por hora, o teto de economia seria 7,5 x 60 = R$ 450/mês.
Meça duas semanas antes e duas depois: volume de pedidos, tempo até a primeira resposta útil e quantidade de solicitações sem contexto. Se o bot só deslocar trabalho para outro canal, o ganho é zero.
Verificação local sem conectar ao Discord
Crie src/verify.js:
import assert from "node:assert/strict";
import { neutralizeMentions } from "./safety.js";
const input = "@everyone <@123> <@&456> @here";
const output = neutralizeMentions(input);
assert.equal(output, "@ everyone user:123 role:456 @ here");
assert.ok(!output.includes("@everyone"));
assert.ok(!output.includes("@here"));
console.log("local safety fixture: ok");
Execute:
npm run verify
Esse comando verifica a sintaxe de bot.js e o comportamento da sanitização. Ele não faz login, não registra comandos e não envia mensagens. Para testar no Discord depois, use um servidor separado, um canal sem dados reais e um token criado só para esse ambiente.
Para continuar com templates e checklists prontos, consulte os produtos do ClaudeCodeLab.
Resultado do teste local
O trecho de segurança foi executado apenas como fixture local: a entrada com @everyone, mention de usuário, mention de cargo e @here virou texto neutro, e as asserções terminaram sem erro. Também foi verificada a sintaxe de src/bot.js com node --check. Não houve login em um Bot real, registro de Slash Commands nem envio para um servidor Discord; esses pontos continuam sendo etapas de homologação humana.
Artigos relacionados
Criar um bot do Slack com Claude Code: triagem, incidentes e relatórios diários
Guia com Bolt JS, Socket Mode, comandos slash, segurança, testes e checklist de produção.
Escolher biblioteca de gráficos com Claude Code
Use Claude Code para escolher Recharts, Chart.js ou D3 e criar dashboards robustos com dados reais.
Claude Code para Vue 3: guia prático com TypeScript e Pinia
Use Claude Code com Vue 3, TypeScript, formulários, Pinia, composables e Vitest em fluxo real de projeto.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.