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.
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.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.