Use Cases (Aktualisiert: 3.6.2026)

Slack Bot mit Claude Code bauen: Triage, Incident-Ersthilfe und Tagesberichte

Praxisguide mit Bolt JS, Socket Mode, Slash Commands, Sicherheit, Tests und Produktionscheckliste.

Slack Bot mit Claude Code bauen: Triage, Incident-Ersthilfe und Tagesberichte

Nicht bei Benachrichtigungen stehen bleiben

Ein Slack Bot ist eine App, die in Slack auf Nachrichten, Slash Commands, Buttons und Modal-Eingaben reagiert. Bolt for JavaScript ist das offizielle Slack-Framework für Node.js und leitet diese Ereignisse an die passende Funktion weiter. Für Einsteiger: Bolt ist das Gerüst, mit dem du ausdrückst, “wenn dieses Slack-Ereignis ankommt, führe diese Funktion aus”.

Der typische Fehler mit Claude Code ist ein Bot, der nur Benachrichtigungen sendet. Ein nützlicher Bot macht aus Kanalrauschen strukturierte Arbeit: Support-Triage, erste Incident-Reaktion, Tagesberichte, Freigaben und Pre-Publish-Checks. In Masas eigenen Workflows war der erste Notification-Bot kurz hilfreich, aber er beantwortete nicht, wer zuständig ist, wie dringend der Fall ist und ob er geschlossen wurde.

Dieser Guide wurde am 3. Juni 2026 mit den offiziellen Slack-Dokumenten abgeglichen: Bolt for JavaScript, Bolt Command Listener, Socket Mode, Slash Commands, Events API, chat.postMessage, Request-Verifizierung und Tokens. Passende ClaudeCodeLab-Artikel sind Webhook-Implementierung, API-Entwicklung, Secrets Management und Workflow-Automatisierung.

Erst die Use Cases festlegen

Wenn du Claude Code nur “baue einen Slack Bot” sagst, entsteht meist eine dünne Demo. Lege vorher fest, welcher Slack-Einstieg genutzt wird, welche Felder gesammelt werden, welche Antwort kommt und wie Fehler aussehen.

Use CaseSlack-EinstiegAufgabe des BotsRisiko
Support-Triage/triage add, ModalTitel, Schweregrad, Anfragende und Kanalnotiz normalisierenKundennamen, Secrets oder private URLs werden eingefügt
Incident-Ersthilfe@bot-Mention, ButtonStart-Checklist zurückgeben und Kontext im Thread haltenDer Bot klingt zu sicher statt zu eskalieren
Tagesbericht/triage list, geplanter JobOffene Punkte für Daily oder Bericht zusammenfassenLange Nachrichten werden in Slack unleserlich
Artikel- oder Landingpage-CheckSlash CommandCTA, interne Links, Owner und Publish-URL prüfenDraft- und Produktions-URLs werden vermischt

Die Architektur bleibt bewusst klein.

flowchart LR
  A["Slack user"] --> B["/triage or @mention"]
  B --> C["Bolt listener"]
  C --> D["Triage logic"]
  D --> E["chat.postMessage"]
  D --> F["Modal and button"]

Ein brauchbarer Prompt für Claude Code:

Implementiere einen Slack Bot mit Bolt for JavaScript.
Ziel ist Support-Triage.
Enthalten sein sollen:
- Umschalten zwischen Socket Mode und Request URL per Umgebungsvariable
- /triage add, /triage list, /triage modal
- Modal-Eingabe und view_submission
- Mark done Button
- Hilfsantwort für app_mention
- Erklärung für scopes, secrets und Request-Verifizierung
- Unit Tests für triage.ts
Keine Pseudo-APIs. Schreibe kopierbares, lauffähiges TypeScript.

Socket Mode oder Request URL

Socket Mode empfängt Ereignisse über eine WebSocket-Verbindung, die deine App zu Slack öffnet. Für lokale Entwicklung brauchst du deshalb keinen öffentlichen HTTPS-Endpunkt. Das passt für Prototypen, interne Tools und Umgebungen hinter Firewalls. Slack beschreibt dafür einen App-Level Token mit xapp-.

Request URL empfängt HTTP POSTs von Slack an deinem HTTPS-Endpunkt. Das ist der übliche Produktionsweg. Bei HTTP musst du die Anfrage mit dem Signing Secret prüfen. Bolt kann diese Prüfung übernehmen, aber im Design sollte stehen, dass alte verification tokens nicht mehr die Basis sind.

ModusGeeignet fürErforderlichStolperfalle
Socket ModeLokale Entwicklung, interner PoCSLACK_APP_TOKEN, connections:writeFällt der Prozess aus, kommen keine Events an; für Marketplace ungeeignet
Request URLHTTP-ProduktionHTTPS-URL, SLACK_SIGNING_SECRETLangsame ack()-Aufrufe führen zu Slack-Timeouts

Starte mit Socket Mode und wechsle zu Request URL, sobald Produktionskanäle oder externe Nutzer betroffen sind. Der Code schaltet mit SLACK_SOCKET_MODE=true.

Slack Manifest und Scopes

Lege das Manifest im Repository ab, damit Dev und Produktion nicht auseinanderlaufen. Die Scopes sind knapp: commands empfängt den Slash Command, chat:write postet Nachrichten und app_mentions:read empfängt Mentions.

display_information:
  name: Claude Triage Bot
  description: Collect triage requests from Slack
  background_color: "#2E2A24"
features:
  bot_user:
    display_name: Claude Triage
    always_online: false
  slash_commands:
    - command: /triage
      description: Add or list triage items
      usage_hint: "add Fix login | list | modal"
      should_escape: true
oauth_config:
  scopes:
    bot:
      - app_mentions:read
      - chat:write
      - commands
settings:
  event_subscriptions:
    bot_events:
      - app_mention
  interactivity:
    is_enabled: true
  socket_mode_enabled: true
  org_deploy_enabled: false
  token_rotation_enabled: false

Füge channels:history oder groups:history nicht vorsorglich hinzu. History-Scopes gehören erst dazu, wenn der Bot wirklich Kanalhistorie liest und Datenschutz sowie Audit geklärt sind.

Lokales Projekt erstellen

Voraussetzung ist Node.js 20 oder neuer.

mkdir claude-slack-triage-bot
cd claude-slack-triage-bot
npm init -y
npm install @slack/bolt @slack/types dotenv
npm install -D typescript tsx vitest @types/node
npm pkg set type=module
npm pkg set scripts.dev="tsx watch src/app.ts"
npm pkg set scripts.build="tsc"
npm pkg set scripts.start="node dist/app.js"
npm pkg set scripts.test="vitest run"
mkdir src tests
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["node"]
  },
  "include": ["src/**/*.ts"]
}

Lege .env.example an. Echte Werte gehören in .env oder in den Secret Manager des Hosters, nie in Git.

SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_SIGNING_SECRET=your-signing-secret
SLACK_SOCKET_MODE=true
SLACK_APP_TOKEN=xapp-your-app-level-token
TRIAGE_CHANNEL_ID=C0123456789
PORT=3000

xoxb- ist der Bot Token. xapp- ist der App-Level Token für Socket Mode. Das Signing Secret belegt, dass HTTP-Requests von Slack kommen. Claude Code braucht diese Werte nicht, sondern Variablennamen, erwartetes Verhalten und Logging-Regeln.

Kopierbare Bolt-Implementierung

Zuerst kommt Slack-unabhängige Logik in src/triage.ts.

// src/triage.ts
import type { KnownBlock, View } from "@slack/types";

export type Severity = "low" | "normal" | "high";

export interface Ticket {
  id: string;
  channelId: string;
  title: string;
  createdBy: string;
  severity: Severity;
  status: "open" | "done";
  createdAt: string;
}

const tickets = new Map<string, Ticket>();

export function resetForTest() {
  tickets.clear();
}

export function parseTriageText(text: string) {
  const [actionRaw, ...rest] = text.trim().split(/\s+/);
  return { action: actionRaw || "help", title: rest.join(" ").trim() };
}

export function addTicket(input: {
  channelId: string;
  title: string;
  createdBy: string;
  severity?: Severity;
}) {
  const ticket: Ticket = {
    id: `triage_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
    channelId: input.channelId,
    title: input.title,
    createdBy: input.createdBy,
    severity: input.severity ?? "normal",
    status: "open",
    createdAt: new Date().toISOString(),
  };
  tickets.set(ticket.id, ticket);
  return ticket;
}

export function completeTicket(id: string) {
  const ticket = tickets.get(id);
  if (!ticket) return undefined;
  const updated: Ticket = { ...ticket, status: "done" };
  tickets.set(id, updated);
  return updated;
}

export function formatTicketList(channelId: string) {
  const open = [...tickets.values()].filter((ticket) => {
    return ticket.channelId === channelId && ticket.status === "open";
  });

  if (open.length === 0) return "No open triage items.";

  return open
    .map((ticket, index) => {
      return `${index + 1}. [${ticket.severity}] ${ticket.title} by <@${ticket.createdBy}>`;
    })
    .join("\n");
}

export function ticketBlocks(ticket: Ticket): KnownBlock[] {
  return [
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: `*${ticket.title}*\nSeverity: ${ticket.severity}\nOwner: <@${ticket.createdBy}>`,
      },
    },
    {
      type: "actions",
      elements: [
        {
          type: "button",
          text: { type: "plain_text", text: "Mark done" },
          action_id: "triage_done",
          value: ticket.id,
        },
      ],
    },
  ];
}

export function modalView(): View {
  return {
    type: "modal",
    callback_id: "triage_modal_submit",
    title: { type: "plain_text", text: "New triage" },
    submit: { type: "plain_text", text: "Create" },
    close: { type: "plain_text", text: "Cancel" },
    blocks: [
      {
        type: "input",
        block_id: "title_block",
        label: { type: "plain_text", text: "What needs attention?" },
        element: {
          type: "plain_text_input",
          action_id: "title_input",
          min_length: 3,
          max_length: 120,
        },
      },
      {
        type: "input",
        block_id: "severity_block",
        label: { type: "plain_text", text: "Severity" },
        element: {
          type: "static_select",
          action_id: "severity_input",
          initial_option: {
            text: { type: "plain_text", text: "Normal" },
            value: "normal",
          },
          options: [
            { text: { type: "plain_text", text: "High" }, value: "high" },
            { text: { type: "plain_text", text: "Normal" }, value: "normal" },
            { text: { type: "plain_text", text: "Low" }, value: "low" },
          ],
        },
      },
    ],
  };
}

Danach werden die Bolt Listener verbunden.

// src/app.ts
import "dotenv/config";
import { App, LogLevel } from "@slack/bolt";
import {
  addTicket,
  completeTicket,
  formatTicketList,
  modalView,
  parseTriageText,
  ticketBlocks,
  type Severity,
} from "./triage.js";

const socketMode = process.env.SLACK_SOCKET_MODE === "true";
const required = ["SLACK_BOT_TOKEN", socketMode ? "SLACK_APP_TOKEN" : "SLACK_SIGNING_SECRET"];

for (const key of required) {
  if (!process.env[key]) throw new Error(`Missing environment variable: ${key}`);
}

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  socketMode,
  appToken: process.env.SLACK_APP_TOKEN,
  logLevel: LogLevel.INFO,
});

app.command("/triage", async ({ ack, command, respond, client }) => {
  await ack();
  const parsed = parseTriageText(command.text);

  if (parsed.action === "add") {
    if (!parsed.title) {
      await respond("Usage: `/triage add Fix login redirect`");
      return;
    }

    const ticket = addTicket({
      channelId: command.channel_id,
      title: parsed.title,
      createdBy: command.user_id,
      severity: "normal",
    });

    await respond({
      response_type: "in_channel",
      text: `Triage item added: ${ticket.title}`,
      blocks: ticketBlocks(ticket),
    });
    return;
  }

  if (parsed.action === "list") {
    await respond({ response_type: "ephemeral", text: formatTicketList(command.channel_id) });
    return;
  }

  if (parsed.action === "modal") {
    await client.views.open({ trigger_id: command.trigger_id, view: modalView() });
    return;
  }

  await respond("Usage: `/triage add ...`, `/triage list`, or `/triage modal`");
});

app.view("triage_modal_submit", async ({ ack, view, body, client }) => {
  const titleState = view.state.values.title_block.title_input;
  const severityState = view.state.values.severity_block.severity_input;
  const title = titleState.type === "plain_text_input" ? titleState.value?.trim() : "";
  const severity =
    severityState.type === "static_select"
      ? severityState.selected_option?.value ?? "normal"
      : "normal";

  if (!title) {
    await ack({ response_action: "errors", errors: { title_block: "Please enter a title." } });
    return;
  }

  await ack();

  const channelId = process.env.TRIAGE_CHANNEL_ID ?? "modal-only";
  const ticket = addTicket({
    channelId,
    title,
    createdBy: body.user.id,
    severity: severity as Severity,
  });

  if (process.env.TRIAGE_CHANNEL_ID) {
    await client.chat.postMessage({
      channel: process.env.TRIAGE_CHANNEL_ID,
      text: `New triage item: ${ticket.title}`,
      blocks: ticketBlocks(ticket),
    });
  }
});

app.action("triage_done", async ({ ack, action, respond }) => {
  await ack();
  const value = action.type === "button" ? action.value : undefined;
  if (!value) return;

  const ticket = completeTicket(value);
  await respond(ticket ? `Closed: ${ticket.title}` : "Ticket not found.");
});

app.event("app_mention", async ({ event, say }) => {
  await say({
    thread_ts: event.ts,
    text: "Use `/triage add ...`, `/triage list`, or `/triage modal`.",
  });
});

const port = Number(process.env.PORT ?? 3000);
if (socketMode) {
  await app.start();
} else {
  await app.start(port);
}

app.logger.info(`Slack bot started in ${socketMode ? "Socket Mode" : `HTTP mode on ${port}`}`);

Unit Tests ohne Slack-Verbindung:

// tests/triage.test.ts
import { beforeEach, describe, expect, it } from "vitest";
import {
  addTicket,
  completeTicket,
  formatTicketList,
  parseTriageText,
  resetForTest,
} from "../src/triage";

describe("triage helpers", () => {
  beforeEach(() => resetForTest());

  it("parses slash command text", () => {
    expect(parseTriageText("add Fix login")).toEqual({
      action: "add",
      title: "Fix login",
    });
  });

  it("lists only open tickets", () => {
    const ticket = addTicket({
      channelId: "C123",
      title: "Review pricing CTA",
      createdBy: "U123",
      severity: "high",
    });

    expect(formatTicketList("C123")).toContain("[high] Review pricing CTA");
    completeTicket(ticket.id);
    expect(formatTicketList("C123")).toBe("No open triage items.");
  });
});

Ausführen:

npm run test
npm run build
npm run dev

In Socket Mode lässt du npm run dev laufen und schreibst /triage add Test from Slack. In Request-URL-Modus deployest du die App und trägst https://example.com/slack/events für Slash Commands, Interactivity und Event Subscriptions ein.

Stolperfallen und Sicherheit

Rufe ack() vor langsamer Arbeit auf. Commands, Buttons und Modals sollten bestätigt werden, bevor Datenbank- oder API-Aufrufe starten.

Behandle trigger_id als kurzlebig. Öffne zuerst das Modal und validiere Details in view_submission.

Suche Berechtigungsfehler nicht nur im Code. Fehlendes chat:write, ein nicht eingeladener Bot oder fehlendes app_mention löst du in Slack Settings.

Mische die Modi nicht. Socket Mode braucht SLACK_APP_TOKEN; Request URL braucht HTTPS und SLACK_SIGNING_SECRET. Logge den Modus beim Start.

Lege niemals Secrets offen. Poste xoxb-, xapp- oder Signing Secret nicht in Claude-Code-Prompts, Screenshots, Logs, Fixtures oder Artikeln. Bei Leaks sofort rotieren.

Gib dem Bot nicht zu viel Urteilskraft. Bei Support und Incidents soll er nächste Prüfschritte und Eskalationsregeln liefern, nicht eine Ursache erfinden.

Produktionscheckliste

  • Manifest-Scopes passen zu den APIs im Code.
  • /triage kollidiert nicht mit einer anderen installierten App.
  • Interactivity ist für Modals und Buttons aktiviert.
  • Der Bot ist im Zielkanal eingeladen.
  • SLACK_BOT_TOKEN, SLACK_APP_TOKEN und SLACK_SIGNING_SECRET liegen als Secrets vor.
  • npm run test und npm run build laufen durch.
  • Request URL nutzt HTTPS und Slack-Signaturprüfung.
  • Socket Mode hat Prozessüberwachung und Neustart.
  • Logs enthalten keine Tokens, unmaskierten personenbezogenen Daten, Kundennamen oder privaten URLs.
  • Im Kanal steht, wer übernimmt, wenn der Bot nicht helfen kann.

Teile die Arbeit für Claude Code auf: Manifest und Scopes, Slack-unabhängige Logik, Bolt Listener, Unit Tests und Deployment-Checkliste. So bleiben Slack-Konfigurationsfehler von Codebugs getrennt.

ClaudeCodeLab behandelt solche internen Bots, Webhooks, APIs und Secrets in Training und Beratung. Wenn du wiederverwendbare CLAUDE.md-Regeln, Pre-Publish-Review-Templates und Team-Checklisten brauchst, kombiniere dieses Muster mit Templates und Produkten, damit der Bot Betrieb und Umsatz unterstützt statt nur eine Demo zu bleiben.

Ergebnis aus dem Praxistest

Der schnellste Weg war nicht, einen großen Slack Bot auf einmal generieren zu lassen. Stabiler war: zuerst Manifest und Scopes fixieren, dann reine Logik wie triage.ts schreiben und zuletzt Bolt Listener mit Slack Admin Settings verbinden. Claude Code funktioniert am besten, wenn Code, Berechtigungen, Secrets, Tests und Produktionscheckliste als eine prüfbare Arbeitseinheit behandelt werden.

#Claude Code #Slack Bot #Bolt SDK #chatbot #automation
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.