Use Cases (Aktualisiert: 3.6.2026)

Discord Bot mit Claude Code bauen: Praxisguide für discord.js Slash Commands

Baue einen Discord Support Bot mit Claude Code, Slash Commands, Rechten, Env Vars und Deploy-Checkliste.

Discord Bot mit Claude Code bauen: Praxisguide für discord.js Slash Commands

Ein Discord Bot ist ein Betriebsweg, nicht nur eine Demo

Ein Discord Bot ist eine Anwendung, die einem Discord-Server beitritt und auf Aktionen von Nutzern reagiert. Einfach gesagt ist er ein automatischer Empfang in deiner Community. Er kann Supportanfragen annehmen, kurze FAQ-Antworten geben, Moderatoren informieren, eine Handoff-Notiz vorbereiten oder einen Vorfall in einen internen Kanal leiten, bevor die Information untergeht.

Das ist nützlicher als ein Bot, der nur nett antwortet. Eine gesunde Community braucht klare Wege für “ich komme nicht weiter”, “wo ist die Anleitung”, “wer übernimmt diesen Fall” und “muss das eskaliert werden”. Wenn diese Fragen im allgemeinen Chat bleiben, verschwinden sie in der Unterhaltung. Mit application commands wird der Prozess sichtbar, wiederholbar und prüfbar.

Claude Code hilft hier am meisten, wenn du nicht nur einen client.login()-Ausschnitt erzeugen lässt. Ein produktionsnaher Bot braucht Command-Registrierung, Umgebungsvariablen, Rechtebegrenzung, Interaction-Antworten, Fehlerbehandlung, Token-Schutz und eine kleine Deployment-Anleitung. Wer nur “baue einen Discord Bot” schreibt, bekommt leicht eine Demo, die nur im Idealfall funktioniert.

Dieser Guide baut einen Support-Bot mit discord.js. Er implementiert /support, /faq und /handoff, nutzt Slash Commands, vermeidet das Message-Content-Intent, entschärft gefährliche Mentions und startet mit Guild Commands im Testserver. Node.js wird in Version 22.12.0 oder neuer vorausgesetzt, passend zur aktuellen discord.js-Dokumentation.

flowchart LR
  A["User runs /support"] --> B["Discord interaction"]
  B --> C["discord.js bot"]
  C --> D["Ephemeral user reply"]
  C --> E["Support channel message"]
  E --> F["Moderator handoff"]

Die wichtigste Entscheidung ist bewusst nüchtern: Die erste Version soll zuverlässig Anfragen aufnehmen. Datenbank, Queue, LLM-Zusammenfassung, CRM-Anbindung oder Abrechnungsprüfung kommen erst danach. Zuerst muss die Schleife funktionieren: Nutzer führt Befehl aus, Bot antwortet privat, Team erhält eine brauchbare Notiz.

Application commands und interactions ohne Fachnebel

Discord application commands sind native Befehle im Discord-Client. Die bekannteste Form ist der Slash Command, etwa /support. Für Support sind sie besser geeignet als alte Prefix-Befehle wie !help, weil Discord Name, Beschreibung, Optionen, Auswahlwerte und Berechtigungsverhalten vor dem Absenden anzeigen kann.

Interactions sind Ereignisse, die deine App erhält, wenn jemand einen Befehl ausführt, einen Button klickt, ein Auswahlmenü nutzt oder ein Modal absendet. Mit discord.js über das Gateway verarbeitet man sie normalerweise in Events.InteractionCreate. Discord kann interactions auch an einen HTTP-Endpunkt senden, aber für ein kleines Team ist ein Gateway Bot lokal einfacher zu starten und zu debuggen.

Die Grundlage sollte immer die offizielle Dokumentation sein. Command-Typen, Namensregeln, Guild Commands, Global Commands und Registrierung stehen in Discord Application Commands. Initiale Antworten, Followups und Interaction Tokens erklärt Receiving and Responding to Interactions. Für Library-API und Node-Anforderungen nutzt du discord.js documentation und den discord.js guide.

Rechte, Env Vars und minimale Architektur

Im Developer Portal erstellst du eine Discord application, fügst einen Bot-User hinzu und erzeugst eine Einladung mit den Scopes bot und applications.commands. Starte nicht mit Administratorrechten. Dieser Bot muss den Supportkanal sehen und dort Nachrichten senden können. /handoff sollte nur für Personen mit Moderatorrechten verfügbar sein, zum Beispiel mit Manage Messages.

ElementWertProduktionshinweis
Node.js22.12.0 oder neuerAktuelle Anforderung von discord.js
OAuth2 scopesbot, applications.commandsNötig für Bot und Slash Commands
Bot permissionsView Channels, Send MessagesMehr nur nach Review
DISCORD_TOKENBot tokenNie committen, screenshotten oder loggen
DISCORD_CLIENT_IDApplication IDFür Command-Registrierung
DISCORD_GUILD_IDTestserver-IDFür Guild Commands in Entwicklung
SUPPORT_CHANNEL_IDInterner SupportkanalBot muss dort senden können

Ein präziser Prompt für Claude Code lautet zum Beispiel: “Erstelle einen Node.js-22-Support-Bot mit discord.js. Er soll /support, /faq und /handoff enthalten, .env nutzen, in der Entwicklung Guild Commands registrieren, minimale Rechte verwenden, mit ephemeral replies antworten und eine Deployment-Checkliste erzeugen.” So entsteht eher ein brauchbarer Startpunkt als ein generischer Chatbot.

Passende interne Vertiefungen sind Umgebungsvariablen verwalten, Fehlerbehandlungsmuster und Code-Review-Checkliste. Der Bot ist klein, aber die Betriebsgewohnheiten sind dieselben wie bei größeren Services.

Ausführbarer discord.js-Starter

Der Starter nutzt JavaScript mit ES modules, damit du ohne TypeScript-Konfiguration beginnen kannst. Wenn DISCORD_GUILD_ID gesetzt ist, werden Befehle im Testserver registriert. Ohne diese Variable werden Global Commands registriert. Nutze DEPLOY_COMMANDS=true beim lokalen Setup; in Produktion sollte Command-Registrierung ein kontrollierter Release-Schritt sein.

mkdir discord-support-bot
cd discord-support-bot
npm init -y
npm install discord.js dotenv
mkdir src

Ergänze type und start in package.json.

{
  "type": "module",
  "scripts": {
    "start": "node src/bot.js"
  },
  "dependencies": {
    "discord.js": "latest",
    "dotenv": "latest"
  }
}

Erstelle .env.

DISCORD_TOKEN=replace_with_bot_token
DISCORD_CLIENT_ID=replace_with_application_id
DISCORD_GUILD_ID=replace_with_test_guild_id
SUPPORT_CHANNEL_ID=replace_with_support_channel_id
DEPLOY_COMMANDS=true

Erstelle src/bot.js.

import "dotenv/config";
import {
  Client,
  Events,
  GatewayIntentBits,
  MessageFlags,
  PermissionFlagsBits,
  REST,
  Routes,
  SlashCommandBuilder,
} from "discord.js";

const token = process.env.DISCORD_TOKEN;
const clientId = process.env.DISCORD_CLIENT_ID;
const guildId = process.env.DISCORD_GUILD_ID;
const supportChannelId = process.env.SUPPORT_CHANNEL_ID;

for (const [name, value] of Object.entries({ token, clientId, supportChannelId })) {
  if (!value) throw new Error(`${name} is required.`);
}

const commands = [
  new SlashCommandBuilder()
    .setName("support")
    .setDescription("Send a support request to the team")
    .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, links, or error messages")
        .setMaxLength(1500),
    ),
  new SlashCommandBuilder()
    .setName("faq")
    .setDescription("Show a short answer for a common topic")
    .addStringOption((option) =>
      option
        .setName("topic")
        .setDescription("FAQ 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 to hand off").setRequired(true),
    )
    .addStringOption((option) =>
      option
        .setName("note")
        .setDescription("What should the next moderator know?")
        .setMaxLength(1500)
        .setRequired(true),
    ),
].map((command) => command.toJSON());

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

client.once(Events.ClientReady, (readyClient) => {
  console.log(`Logged in as ${readyClient.user.tag}`);
});

client.on(Events.InteractionCreate, async (interaction) => {
  if (!interaction.isChatInputCommand()) return;

  try {
    if (!interaction.inGuild()) {
      await interaction.reply({
        content: "Please use this command inside the server.",
        flags: MessageFlags.Ephemeral,
      });
      return;
    }

    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 safeReply(interaction, "Unknown command.");
  } catch (error) {
    console.error("Interaction failed:", error);
    await safeReply(interaction, "Something went wrong. Please contact a moderator.");
  }
});

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 fetchSupportChannel();

  await channel.send({
    content: [
      "**New support request**",
      `Reporter: ${interaction.user.tag} (${interaction.user.id})`,
      `Severity: ${severity}`,
      `Channel: <#${interaction.channelId}>`,
      `Summary: ${neutralizeMentions(summary)}`,
      `Context: ${neutralizeMentions(context)}`,
    ].join("\n"),
    allowedMentions: { parse: [] },
  });

  await interaction.reply({
    content: "Thanks. Your request was sent to the support team.",
    flags: MessageFlags.Ephemeral,
  });
}

async function handleFaq(interaction) {
  const topic = interaction.options.getString("topic", true);
  const answers = {
    setup: "Install Node.js 22.12+, invite the bot with bot and applications.commands scopes, then run npm start.",
    permissions: "Start with View Channels and Send Messages. Reserve Manage Messages for moderator-only commands.",
    rollout: "Use guild commands for testing. Promote to global commands only after rollback and logging are checked.",
  };

  await interaction.reply({
    content: answers[topic],
    flags: MessageFlags.Ephemeral,
  });
}

async function handleHandoff(interaction) {
  if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageMessages)) {
    await interaction.reply({
      content: "You need Manage Messages permission to use this command.",
      flags: MessageFlags.Ephemeral,
    });
    return;
  }

  const target = interaction.options.getUser("target", true);
  const note = interaction.options.getString("note", true);
  const channel = await fetchSupportChannel();

  await channel.send({
    content: [
      "**Moderator handoff**",
      `Target: ${target.tag} (${target.id})`,
      `From: ${interaction.user.tag} (${interaction.user.id})`,
      `Note: ${neutralizeMentions(note)}`,
    ].join("\n"),
    allowedMentions: { parse: [] },
  });

  await interaction.reply({
    content: "Handoff note created.",
    flags: MessageFlags.Ephemeral,
  });
}

async function fetchSupportChannel() {
  const channel = await client.channels.fetch(supportChannelId);
  if (!channel || !channel.isTextBased() || typeof channel.send !== "function") {
    throw new Error("SUPPORT_CHANNEL_ID must be a text channel the bot can send to.");
  }
  return channel;
}

function neutralizeMentions(value) {
  return value
    .replaceAll("@everyone", "@ everyone")
    .replaceAll("@here", "@ here")
    .replace(/<@!?(\d+)>/g, "user:$1")
    .replace(/<@&(\d+)>/g, "role:$1");
}

async function safeReply(interaction, content) {
  const payload = { content, flags: MessageFlags.Ephemeral };
  if (interaction.replied || interaction.deferred) await interaction.followUp(payload);
  else await interaction.reply(payload);
}

async function deployCommands() {
  const rest = new REST({ version: "10" }).setToken(token);
  const route = guildId
    ? Routes.applicationGuildCommands(clientId, guildId)
    : Routes.applicationCommands(clientId);

  await rest.put(route, { body: commands });
  console.log(guildId ? "Guild commands deployed." : "Global commands deployed.");
}

if (process.env.DEPLOY_COMMANDS === "true") {
  await deployCommands();
}

await client.login(token);

Lokal prüfst du zuerst node --version und startest dann npm start. Teste /support: Der Nutzer soll eine private Antwort sehen, während der interne Kanal eine strukturierte Nachricht erhält. Danach testest du /faq. Zum Schluss prüfst du /handoff mit einem Moderator und einem normalen Nutzer.

Use case: drei Workflows, die den Bot rechtfertigen

Der erste Use case ist Supportannahme. Ein guter Intake-Befehl braucht keine zwanzig Felder. Summary, severity und context reichen für Triage, also die Entscheidung, ob ein Thema beantwortet, reproduziert, zugewiesen oder eskaliert wird. In einem kleinen Testserver reduzierte dieses Format Rückfragen, weil Dringlichkeit, Symptom und Reproduktionshinweis schon in der ersten Nachricht standen.

Der zweite Use case ist FAQ-Routing. Der Bot sollte keine komplette Dokumentationsseite in den Chat kopieren. Besser ist eine kurze Antwort mit dem richtigen Link. Setup-Fragen führen zum Claude Code Einstieg, CLI-Fragen zu CLI-Tool-Entwicklung und Teamregeln zu CLAUDE.md Vorlagen. Der Bot wird so zur Navigation, nicht zu einer zweiten Dokumentation.

Der dritte Use case ist Moderator-Handoff. In bezahlten Communities, Trainingskohorten, Gameservern oder Produktsupport wechseln Verantwortliche. “Kann jemand schauen?” verliert schnell Kontext. /handoff hält Zielnutzer, Autor und nächste Information in einer internen Notiz fest.

Ein vierter Fall ist Trainingssupport. In einem Claude-Code-Workshop treffen viele auf dieselben Node-Versionen, Env-Variablen oder Kommando-Fehler. Wenn der Bot zuerst Version, ausgeführten Befehl und letzte Logzeilen einsammelt, kann der Trainer direkt diagnostizieren. Für Teams ist der passende nächste Schritt Training und Beratung.

Pitfall: Produktionsfehler, die früh raus müssen

Der erste Pitfall ist ein geleakter Bot Token. Wenn er in Git, Screenshots, CI-Logs oder Dokumentation auftaucht, gilt er als kompromittiert. Rotiere ihn im Developer Portal und entferne den alten Wert. Claude Code sollte ausdrücklich angewiesen werden, nur .env.example mit Platzhaltern zu erzeugen und echte Secrets nie auszugeben.

Der zweite Fehler ist ständige Global-Command-Registrierung während der Entwicklung. Guild Commands sind schnell und auf den Testserver begrenzt. Global Commands sind Produktion. Veröffentliche sie erst, wenn Namen, Beschreibungen, Rechte und Rollback geklärt sind. Registriere Commands auch nicht bei jedem Produktionsneustart neu, außer das ist ein kontrollierter Deploy-Schritt.

Der dritte Fehler ist ein Interaction-Zweig ohne Antwort. Für Nutzer wirkt das wie ein kaputter Befehl. Jeder Pfad muss reply, defer oder follow up ausführen, auch Fehler und unbekannte Befehle. Wenn du später eine externe API oder LLM-Zusammenfassung einbaust, defer zuerst und sende danach das Ergebnis.

Der vierte Fehler ist Mention-Missbrauch. Wenn Nutzereingaben unverändert in einen internen Kanal gehen, können @everyone, @here, Nutzer- und Rollenmentions Benachrichtigungsunfälle auslösen. Das Beispiel nutzt allowedMentions: { parse: [] } und Textbereinigung. Beides sollte bleiben.

Der fünfte Fehler ist Administratorrecht aus Bequemlichkeit. Das versteckt fehlende Rechteentscheidungen und vergrößert den Schaden bei Token-Diebstahl. Starte mit View Channels und Send Messages. Füge nur Rechte hinzu, die du im Review erklären kannst.

Sicherheits- und Deployment-Checkliste

  • Eine verantwortliche Person kann den Token im Developer Portal rotieren
  • .env ist nicht in Git, .env.example enthält nur Platzhalter
  • Die Einladung startet mit bot und applications.commands
  • Bot permissions beginnen mit View Channels und Send Messages
  • /handoff ist auf Moderatoren beschränkt
  • Guild Commands werden vor Global Commands getestet
  • Jeder Interaction-Pfad replyt, defert oder followt up
  • Nutzereingaben können keine Mentions auslösen
  • Logs enthalten keine Token, Zahlungsdaten, E-Mails oder privaten Supportdaten
  • Das Hosting nutzt Node.js 22.12.0 oder neuer
  • Neustart und Rollback sind dokumentiert

Ein kleiner Bot kann auf Railway, Render, Fly.io, einem VPS oder einem internen Server laufen. Wichtiger als die Plattform sind getrennte Secrets, sichtbare Logs und ein dokumentierter Neustart. Lass Claude Code das README mit genau diesen Abschnitten erstellen.

Für wiederverwendbare Vorlagen eignen sich ClaudeCodeLab Produkte. Wenn dein Team Discord-Support, Claude-Code-Einführung, Rechte, Reviews und Training gemeinsam strukturieren will, ist Training und Beratung der nächste Schritt. Für dauerhafte Wartung sollte der Bot außerdem mit Code-Review-Automatisierung verbunden werden.

Ergebnis nach dem Test

Im kleinen Testserver war der größte Gewinn nicht die Intelligenz des Bots, sondern die Form der Anfrage. Freie Nachrichten beginnen oft mit “geht nicht”. /support erzwingt summary, severity und context. Dadurch war die erste Moderatorantwort häufiger eine Diagnose statt einer Rückfrage. Die riskanten Stellen waren Token, Global Commands, Mentions und Rechte. Claude Code war am nützlichsten, wenn es nicht nur Code schrieb, sondern auch .env.example, Rechttabelle, Fehlerantworten und Deployment-Checkliste erzeugte.

#Claude Code #Discord Bot #discord.js #Chatbot #Community
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.