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.
Não pare em um bot de notificações
Um Slack Bot é um app que reage a mensagens, comandos slash, botões e envios de modal dentro do Slack. Bolt for JavaScript é o framework oficial da Slack para Node.js que envia cada evento ao handler correto. Em termos simples, Bolt é a estrutura que permite dizer: “quando este evento do Slack chegar, execute esta função”.
O erro comum com Claude Code é parar em um bot que apenas envia alertas. Um bot útil transforma ruído de canal em trabalho estruturado: triagem de suporte, primeira resposta a incidentes, relatórios diários, aprovações e checagens antes de publicar. Nos fluxos de Masa, o primeiro bot de notificação ajudou por alguns dias, mas não registrava quem era o responsável, qual era a urgência nem se o item já tinha sido encerrado.
Este guia foi conferido com a documentação oficial da Slack em 3 de junho de 2026: Bolt for JavaScript, listeners de comandos no Bolt, Socket Mode, comandos slash, Events API, chat.postMessage, verificação de requests e tokens. Para contexto interno, veja também webhooks com Claude Code, desenvolvimento de APIs, gestão de secrets e automação de workflows.
Defina os casos de uso antes
Se você pedir a Claude Code “crie um Slack bot” sem um fluxo concreto, o resultado tende a ser uma demo rasa. Defina antes a entrada no Slack, os campos coletados, a resposta e o comportamento de falha.
| Caso de uso | Entrada no Slack | O que o bot faz | Risco a controlar |
|---|---|---|---|
| Triagem de suporte | /triage add, modal | Normaliza título, severidade, solicitante e notificação de canal | Usuários colam nomes de clientes, secrets ou URLs privadas |
| Primeira resposta a incidente | Menção @bot, botão | Retorna checklist inicial e mantém contexto em thread | O bot parece confiante demais em vez de escalar |
| Relatório diário | /triage list, job agendado | Resume itens abertos para daily ou relatório | Mensagens longas ficam pouco legíveis no Slack |
| Checagem de artigo ou landing page | Slash Command | Verifica CTA, links internos, responsável e URL de publicação | URLs de rascunho e produção se misturam |
A arquitetura é propositalmente pequena.
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"]
Um prompt bom para Claude Code:
Implemente um Slack Bot com Bolt for JavaScript.
O objetivo é triagem de suporte.
Inclua:
- Alternar entre Socket Mode e Request URL por variáveis de ambiente
- /triage add, /triage list, /triage modal
- Entrada de modal e tratamento de view_submission
- Botão Mark done
- Resposta de ajuda para app_mention
- Explicação de scopes, secrets e verificação de requests
- Testes unitários para triage.ts
Não use APIs fictícias. Escreva TypeScript copiável e executável.
Socket Mode ou Request URL
Socket Mode recebe eventos por uma conexão WebSocket iniciada pelo seu app, então você não precisa expor um endpoint HTTPS público no desenvolvimento local. É útil para protótipos, ferramentas internas e ambientes atrás de firewall. A documentação da Slack explica o uso de um app-level token que começa com xapp-.
Request URL recebe POSTs HTTP da Slack no seu endpoint HTTPS. Esse é o padrão comum em produção. Ao receber HTTP, verifique a assinatura com o Signing Secret. O Bolt pode fazer isso quando configurado, mas o design deve registrar que verification tokens antigos não são a base.
| Modo | Melhor para | Configuração necessária | Armadilha |
|---|---|---|---|
| Socket Mode | Desenvolvimento local, PoC interno | SLACK_APP_TOKEN, connections:write | Se o processo cair, não há eventos; não é ideal para Marketplace |
| Request URL | Deploy HTTP de produção | URL HTTPS, SLACK_SIGNING_SECRET | ack() lento vira timeout no Slack |
Comece com Socket Mode e migre para Request URL quando o bot tocar canais de produção ou usuários externos. O código muda com SLACK_SOCKET_MODE=true.
Manifest e scopes do Slack
Mantenha o manifest no repositório para evitar divergência entre dev e produção. Os scopes são mínimos: commands recebe o slash command, chat:write publica mensagens e app_mentions:read recebe menções.
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ão adicione channels:history ou groups:history só porque parecem úteis. Scopes de leitura de histórico só entram quando o bot realmente lê histórico e alguém revisou privacidade.
Criar o projeto local
Use Node.js 20 ou mais novo.
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"]
}
Crie .env.example. Valores reais ficam em .env ou no secret manager da hospedagem, nunca no 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- é o token do bot. xapp- é o app-level token usado pelo Socket Mode. O Signing Secret prova que requests HTTP vieram da Slack. Claude Code não precisa desses valores reais, apenas dos nomes das variáveis, comportamento esperado e regras de log.
Implementação Bolt copiável
Primeiro, isole a lógica sem dependência da Slack em 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" },
],
},
},
],
};
}
Agora conecte os listeners do 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}`}`);
Adicione testes unitários sem 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.");
});
});
Execute:
npm run test
npm run build
npm run dev
Com Socket Mode, deixe npm run dev rodando e digite /triage add Test from Slack. Com Request URL, faça deploy e configure https://example.com/slack/events em slash commands, interactivity e event subscriptions.
Armadilhas e segurança
Chame ack() antes de trabalho lento. Comandos, botões e modais devem confirmar recebimento antes de banco de dados ou APIs externas.
Trate trigger_id como curto. Abra o modal primeiro e valide os detalhes em view_submission.
Não depure permissões apenas no código. Falta de chat:write, bot fora do canal ou ausência de app_mention são problemas de configuração na Slack.
Não misture modos. Socket Mode precisa de SLACK_APP_TOKEN; Request URL precisa de HTTPS e SLACK_SIGNING_SECRET. Registre o modo no startup.
Nunca exponha secrets. Não cole xoxb-, xapp- ou Signing Secret em prompts do Claude Code, screenshots, logs, fixtures ou artigos. Se vazar, rotacione imediatamente.
Por fim, não dê julgamento demais ao bot. Em suporte e incidentes, ele deve mostrar próximos passos e regra de escalonamento, não inventar causa raiz.
Checklist de produção
- Scopes do manifest batem com as APIs usadas.
/triagenão conflita com outro app instalado.- Interactivity está ativa para modais e botões.
- O bot foi convidado para o canal de destino.
SLACK_BOT_TOKEN,SLACK_APP_TOKENeSLACK_SIGNING_SECRETestão como secrets.npm run testenpm run buildpassam.- Request URL usa HTTPS e verificação de assinatura Slack.
- Socket Mode tem monitoramento de processo e restart.
- Logs não incluem tokens, dados pessoais sem máscara, clientes ou URLs privadas.
- O tópico do canal explica quem recebe escalonamento quando o bot não consegue ajudar.
Separe o que Claude Code deve produzir: proposta de manifest e scopes, lógica sem Slack, listeners Bolt, testes unitários e checklist de deploy. Assim erros de configuração não se misturam com bugs de código.
ClaudeCodeLab cobre bots internos, webhooks, APIs e secrets em treinamento e consultoria. Para regras CLAUDE.md reutilizáveis, templates de revisão antes de publicar e checklists de equipe, combine este padrão com templates e produtos para apoiar operação e receita, não apenas uma demo.
Resultado ao testar
O caminho mais rápido não foi gerar um bot grande de uma vez. O fluxo mais estável foi travar manifest e scopes primeiro, escrever lógica pura como triage.ts depois, e só então conectar listeners Bolt com as configurações da Slack. Claude Code funciona melhor quando também revisa permissões, secrets, testes e checklist de produção como uma unidade de trabalho.
Artigos relacionados
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.
Codex Automations para content ops: QA, deploy e conversão diária
Use Codex Automations para revisar analytics, escolher temas, fazer QA, verificar deploy e melhorar CTAs.
Ambiente de desenvolvimento com Claude Code: seguro e reproduzível
Configure Claude Code com Node, Docker, .env, permissões, hooks e verificações reproduzíveis.
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.