Use Cases (Atualizado: 21/07/2026)

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.

Criar um Discord Bot com Claude Code: Slash Commands com discord.js

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.js 14.27.0 e dotenv 17.2.3;
  • registro de comandos apenas no servidor de teste;
  • deferReply() seguido de editReply() 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 prepararUma pessoa precisa decidir
Estrutura do projeto e Slash CommandsQuem pode usar /handoff
Validação de variáveis e testes locaisQual canal recebe dados privados
Sanitização de mentions e tratamento de errosQuanto tempo guardar mensagens e logs
Checklist de deploy e rollbackQuando 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

AntesDepois
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çõesallowedMentions 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.

#Claude Code #Discord Bot #discord.js #chatbot #comunidade
Grátis

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.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.