Use Cases (Mis à jour: 03/06/2026)

Créer un bot Slack avec Claude Code : triage, incidents et rapports quotidiens

Guide Bolt JS avec Socket Mode, commandes slash, sécurité, tests et checklist de production.

Créer un bot Slack avec Claude Code : triage, incidents et rapports quotidiens

Ne vous arrêtez pas au bot de notification

Un bot Slack est une application qui réagit aux messages, commandes slash, boutons et soumissions de modales dans Slack. Bolt for JavaScript est le framework officiel de Slack pour Node.js : il route chaque événement vers le bon handler. Dit simplement, Bolt est l’échafaudage qui permet d’écrire “quand cet événement Slack arrive, exécute cette fonction”.

L’erreur fréquente avec Claude Code consiste à générer un petit bot qui envoie seulement des notifications. Un bot réellement utile transforme le bruit d’un canal en travail structuré : triage de demandes, première réponse à incident, rapport quotidien, validation et contrôle avant publication. Dans les workflows de Masa, le premier bot de notification était pratique, mais il ne disait pas qui était responsable, quelle était l’urgence ni si le sujet était clos.

Ce guide a été vérifié avec la documentation officielle Slack le 3 juin 2026 : Bolt for JavaScript, listeners de commandes Bolt, Socket Mode, commandes slash, Events API, chat.postMessage, vérification des requêtes et tokens. Pour le contexte ClaudeCodeLab, lisez aussi l’implémentation webhook, le développement API, la gestion des secrets et l’automatisation de workflows.

Choisir les cas d’usage d’abord

Si vous demandez simplement à Claude Code de “créer un bot Slack”, vous obtiendrez souvent une démo trop mince. Décidez d’abord le point d’entrée Slack, les champs à collecter, le message de retour et le comportement en cas d’échec.

Cas d’usageEntrée SlackCe que fait le botRisque à contrôler
Triage support/triage add, modaleNormalise titre, gravité, demandeur et notification de canalDes noms clients, secrets ou URLs privées sont collés
Première réponse incidentMention @bot, boutonRetourne une checklist initiale et garde le contexte dans un threadLe bot répond avec trop de certitude au lieu d’escalader
Rapport quotidien/triage list, tâche planifiéeRésume les sujets ouverts pour le daily ou le rapportLes messages longs deviennent illisibles dans Slack
Contrôle article ou landing pageSlash CommandVérifie CTA, liens internes, responsable et URL de publicationMélange entre brouillon et URL de production

L’architecture reste volontairement petite.

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"]

Voici un prompt précis pour Claude Code :

Implémente un Slack Bot avec Bolt for JavaScript.
L'objectif est le triage support.
Inclure:
- Bascule entre Socket Mode et Request URL par variables d'environnement
- /triage add, /triage list, /triage modal
- Saisie de modale et gestion view_submission
- Bouton Mark done
- Réponse d'aide pour app_mention
- Explication des scopes, secrets et vérification de requête
- Tests unitaires pour triage.ts
N'utilise pas de fausses APIs. Écris du TypeScript copiable et exécutable.

Socket Mode ou Request URL

Socket Mode reçoit les événements via une connexion WebSocket initiée par votre application. Vous n’avez donc pas besoin d’un endpoint HTTPS public en développement local. C’est utile pour les prototypes, les outils internes et les environnements derrière un pare-feu. La documentation Slack décrit l’activation de Socket Mode et l’utilisation d’un app-level token commençant par xapp-.

Request URL reçoit des requêtes HTTP POST de Slack sur votre endpoint HTTPS. C’est le modèle courant en production. En HTTP, vérifiez la signature avec le Signing Secret. Bolt peut faire cette vérification si vous le configurez, mais la note de conception doit préciser de ne pas dépendre des anciens verification tokens.

ModeUsage idéalConfiguration nécessairePiège
Socket ModeDéveloppement local, PoC interneSLACK_APP_TOKEN, connections:writePlus d’événements si le processus tombe ; peu adapté au Marketplace
Request URLDéploiement HTTP de productionURL HTTPS, SLACK_SIGNING_SECRETUn ack() lent provoque un timeout Slack

Commencez avec Socket Mode, puis passez à Request URL lorsque le bot touche des canaux de production ou des utilisateurs externes. Le code ci-dessous bascule avec SLACK_SOCKET_MODE=true.

Manifest Slack et scopes

Gardez le manifest dans le dépôt pour éviter les écarts entre dev et production. Les scopes sont volontairement limités : commands reçoit la commande slash, chat:write publie des messages et app_mentions:read reçoit les 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

N’ajoutez pas channels:history ou groups:history par confort. Les scopes de lecture d’historique ne se justifient que si le bot lit vraiment l’historique et si l’impact confidentialité a été revu.

Créer le projet local

Prévoyez Node.js 20 ou plus récent.

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"]
}

Ajoutez .env.example. Les vraies valeurs vont dans .env ou dans le gestionnaire de secrets de l’hébergeur.

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- est le token du bot. xapp- est l’app-level token utilisé par Socket Mode. Le Signing Secret prouve qu’une requête HTTP vient bien de Slack. Claude Code n’a pas besoin des valeurs réelles, seulement des noms de variables, du comportement attendu et des règles de logs.

Implémentation Bolt copiable

Commencez par isoler la logique sans dépendance Slack dans 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" },
          ],
        },
      },
    ],
  };
}

Connectez ensuite les listeners Bolt.

// 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}`}`);

Ajoutez des tests unitaires sans connexion Slack.

// 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.");
  });
});

Exécutez :

npm run test
npm run build
npm run dev

En Socket Mode, laissez npm run dev actif et tapez /triage add Test from Slack. En Request URL, déployez l’app puis configurez https://example.com/slack/events pour les commandes slash, l’interactivité et les abonnements d’événements.

Pièges et sécurité

Appelez ack() avant les tâches lentes. Une commande, un bouton ou une modale doit être acquitté avant l’écriture en base ou les appels API externes.

Considérez trigger_id comme très court. Ouvrez la modale d’abord, puis validez les détails dans view_submission.

Ne cherchez pas les problèmes de permission uniquement dans le code. Un chat:write absent, un bot non invité ou un abonnement app_mention manquant se corrigent dans Slack.

Ne mélangez pas les modes. Socket Mode demande SLACK_APP_TOKEN; Request URL demande HTTPS et SLACK_SIGNING_SECRET. Logguez le mode choisi au démarrage.

N’exposez jamais les secrets. Ne collez pas xoxb-, xapp- ou le Signing Secret dans les prompts Claude Code, captures, logs, fixtures ou articles. Faites une rotation immédiate en cas de fuite.

Enfin, ne donnez pas trop de jugement au bot. Pour support et incidents, il doit donner les prochaines vérifications et la règle d’escalade, pas prétendre connaître la cause racine.

Checklist production

  • Les scopes du manifest correspondent aux APIs utilisées.
  • /triage n’entre pas en conflit avec une autre app installée.
  • L’interactivité est active pour modales et boutons.
  • Le bot est invité dans le canal cible.
  • SLACK_BOT_TOKEN, SLACK_APP_TOKEN et SLACK_SIGNING_SECRET sont stockés comme secrets.
  • npm run test et npm run build passent.
  • Request URL utilise HTTPS et la vérification de signature Slack.
  • Socket Mode a une surveillance de processus et un redémarrage.
  • Les logs ne contiennent pas tokens, données personnelles non masquées, noms clients ou URLs privées.
  • Le sujet du canal indique à qui escalader quand le bot ne peut pas répondre.

Séparez ce que Claude Code doit produire : proposition de manifest et scopes, logique indépendante de Slack, listeners Bolt, tests unitaires et checklist de déploiement. Vous évitez ainsi de mélanger erreurs de configuration Slack et bugs de code.

ClaudeCodeLab couvre ces bots internes, webhooks, APIs et secrets dans ses offres de formation et conseil. Pour obtenir des règles CLAUDE.md, modèles de revue avant publication et checklists d’équipe, combinez ce pattern avec les templates et produits afin de soutenir les opérations et les revenus, pas seulement une démo.

Résultat du test

Le chemin le plus rapide n’a pas été de générer un grand bot Slack d’un seul coup. Le plus stable a été de figer d’abord le manifest et les scopes, d’écrire ensuite la logique pure comme triage.ts, puis de connecter les listeners Bolt et la configuration Slack. Claude Code est plus fiable lorsqu’il traite code, permissions, secrets, tests et checklist de production comme une seule unité révisable.

#Claude Code #Slack Bot #Bolt SDK #chatbot #automation
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.