Membuat Slack Bot dengan Claude Code: triase, insiden, dan laporan harian
Panduan Bolt JS dengan Socket Mode, slash command, keamanan, test, dan checklist produksi.
Jangan berhenti di bot notifikasi
Slack Bot adalah aplikasi yang merespons pesan, slash command, tombol, dan pengiriman modal di dalam Slack. Bolt for JavaScript adalah framework resmi Slack untuk Node.js yang mengarahkan event tersebut ke handler yang tepat. Untuk pemula, Bolt bisa dianggap sebagai kerangka yang membuat aturan “saat event Slack ini datang, jalankan fungsi ini”.
Kesalahan umum saat memakai Claude Code adalah berhenti pada bot yang hanya mengirim notifikasi. Bot yang berguna mengubah percakapan kanal menjadi pekerjaan terstruktur: triase support, respons pertama insiden, laporan harian, approval, dan pemeriksaan sebelum publikasi. Dalam alur kerja Masa, bot notifikasi pertama memang membantu beberapa hari, tetapi tidak menjawab siapa pemilik tugas, seberapa urgent, dan apakah sudah selesai.
Panduan ini diverifikasi dengan dokumentasi resmi Slack pada 3 Juni 2026: Bolt for JavaScript, listener command Bolt, Socket Mode, slash command, Events API, chat.postMessage, verifikasi request, dan tokens. Untuk konteks ClaudeCodeLab, baca juga implementasi webhook, pengembangan API, manajemen secrets, dan otomasi workflow.
Tentukan use case terlebih dahulu
Jika Anda hanya meminta Claude Code “buat Slack bot”, hasilnya biasanya demo tipis. Tentukan dulu titik masuk di Slack, field yang dikumpulkan, pesan balasan, dan perilaku saat gagal.
| Use case | Titik masuk Slack | Yang dilakukan bot | Risiko yang dikendalikan |
|---|---|---|---|
| Triase support | /triage add, modal | Menormalkan judul, severity, pemohon, dan notifikasi kanal | Pengguna menempel nama pelanggan, secret, atau URL privat |
| Respons pertama insiden | Mention @bot, tombol | Mengirim checklist awal dan menyimpan konteks di thread | Bot terdengar terlalu yakin dan tidak mengeskalasi |
| Laporan harian | /triage list, job terjadwal | Merangkum item terbuka untuk daily atau laporan | Pesan terlalu panjang dan sulit dibaca di Slack |
| Cek artikel atau landing page | Slash Command | Mengecek CTA, internal link, pemilik, dan URL publikasi | URL draft dan produksi tercampur |
Arsitekturnya sengaja kecil.
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"]
Prompt yang konkret untuk Claude Code:
Implementasikan Slack Bot dengan Bolt for JavaScript.
Tujuannya adalah triase support.
Sertakan:
- Beralih antara Socket Mode dan Request URL dengan environment variable
- /triage add, /triage list, /triage modal
- Input modal dan handling view_submission
- Tombol Mark done
- Balasan bantuan untuk app_mention
- Penjelasan scopes, secrets, dan verifikasi request
- Unit test untuk triage.ts
Jangan gunakan pseudo API. Tulis TypeScript yang bisa dicopy dan dijalankan.
Socket Mode atau Request URL
Socket Mode menerima event melalui koneksi WebSocket yang dibuka oleh aplikasi Anda, sehingga pengembangan lokal tidak membutuhkan endpoint HTTPS publik. Ini cocok untuk prototype, tool internal, dan lingkungan di balik firewall. Dokumentasi Slack menjelaskan penggunaan app-level token yang diawali xapp-.
Request URL menerima HTTP POST dari Slack ke endpoint HTTPS Anda. Ini pola umum untuk produksi. Saat memakai HTTP, verifikasi signature dengan Signing Secret. Bolt bisa melakukan verifikasi jika dikonfigurasi, tetapi catatan desain tetap harus mengatakan bahwa verification token lama tidak menjadi dasar keamanan.
| Mode | Cocok untuk | Konfigurasi yang dibutuhkan | Jebakan |
|---|---|---|---|
| Socket Mode | Dev lokal, PoC internal | SLACK_APP_TOKEN, connections:write | Jika proses mati, event tidak masuk; kurang cocok untuk Marketplace |
| Request URL | Deploy HTTP produksi | URL HTTPS, SLACK_SIGNING_SECRET | ack() yang lambat menjadi timeout di Slack |
Mulai dengan Socket Mode, lalu pindah ke Request URL ketika bot menyentuh kanal produksi atau pengguna eksternal. Kode di bawah berpindah dengan SLACK_SOCKET_MODE=true.
Manifest dan scopes Slack
Simpan manifest di repository agar konfigurasi dev dan produksi tidak berbeda diam-diam. Scope di sini minimal: commands untuk menerima slash command, chat:write untuk mengirim pesan, dan app_mentions:read untuk menerima mention.
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
Jangan menambahkan channels:history atau groups:history hanya karena terlihat berguna. Scope baca histori baru masuk jika bot benar-benar membaca histori dan dampak privasinya sudah direview.
Membuat proyek lokal
Gunakan Node.js 20 atau lebih baru.
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"]
}
Buat .env.example. Nilai asli disimpan di .env atau secret manager hosting, bukan di 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- adalah bot token. xapp- adalah app-level token untuk Socket Mode. Signing Secret membuktikan request HTTP berasal dari Slack. Claude Code tidak membutuhkan nilai asli, hanya nama variabel, perilaku yang diharapkan, dan aturan logging.
Implementasi Bolt yang bisa dicopy
Pisahkan dulu logika yang tidak bergantung pada Slack ke 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" },
],
},
},
],
};
}
Lalu hubungkan listener 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}`}`);
Tambahkan unit test tanpa 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.");
});
});
Jalankan:
npm run test
npm run build
npm run dev
Dengan Socket Mode, biarkan npm run dev berjalan dan ketik /triage add Test from Slack. Dengan Request URL, deploy app lalu set https://example.com/slack/events untuk slash commands, interactivity, dan event subscriptions.
Jebakan dan keamanan
Panggil ack() sebelum pekerjaan lambat. Command, tombol, dan modal harus mengonfirmasi penerimaan sebelum update database atau call API eksternal.
Anggap trigger_id berumur pendek. Buka modal dulu, lalu validasi detail di view_submission.
Jangan debug masalah izin hanya dari kode. chat:write yang kurang, bot belum diundang, atau subscription app_mention yang belum ada harus diperbaiki di Slack settings.
Jangan mencampur mode. Socket Mode butuh SLACK_APP_TOKEN; Request URL butuh HTTPS dan SLACK_SIGNING_SECRET. Tampilkan mode yang dipakai saat startup.
Jangan pernah membocorkan secret. Jangan tempel xoxb-, xapp-, atau Signing Secret ke prompt Claude Code, screenshot, log, fixture, atau artikel. Jika bocor, rotasi segera.
Terakhir, jangan memberi bot terlalu banyak wewenang mengambil kesimpulan. Untuk support dan insiden, bot sebaiknya memberi langkah cek berikutnya dan aturan eskalasi, bukan mengarang root cause.
Checklist produksi
- Scope manifest cocok dengan API yang dipakai.
/triagetidak bentrok dengan app lain.- Interactivity aktif untuk modal dan tombol.
- Bot sudah diundang ke kanal tujuan.
SLACK_BOT_TOKEN,SLACK_APP_TOKEN, danSLACK_SIGNING_SECRETtersimpan sebagai secrets.npm run testdannpm run buildlulus.- Request URL memakai HTTPS dan verifikasi signature Slack.
- Socket Mode memiliki process monitoring dan restart.
- Log tidak berisi token, data personal tanpa masking, nama pelanggan, atau URL privat.
- Topik kanal menjelaskan siapa yang menerima eskalasi saat bot tidak bisa membantu.
Pisahkan pekerjaan Claude Code: usulan manifest dan scopes, logika tanpa Slack, listener Bolt, unit test, dan checklist deploy. Dengan begitu, kesalahan konfigurasi Slack tidak bercampur dengan bug kode.
ClaudeCodeLab membahas bot internal, webhook, API, dan secrets dalam training dan konsultasi. Jika Anda butuh aturan CLAUDE.md, template review sebelum publikasi, dan checklist tim, gabungkan pola ini dengan template dan produk agar bot membantu operasi dan monetisasi, bukan hanya demo.
Hasil saat diuji
Jalur tercepat bukan membuat bot Slack besar dalam sekali generate. Cara yang paling stabil adalah mengunci manifest dan scopes terlebih dahulu, menulis logika murni seperti triage.ts, lalu menghubungkan listener Bolt dengan konfigurasi admin Slack. Claude Code paling berguna ketika kode, izin, secrets, test, dan checklist produksi diperlakukan sebagai satu unit kerja yang bisa direview.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Workflow Obsidian ke CLAUDE.md untuk Claude Code
Ubah catatan kerja Obsidian menjadi operating note CLAUDE.md agar konteks tidak dijelaskan ulang.
Claude Code Revenue CTA Routing: dari artikel ke PDF, Gumroad, dan konsultasi
Workflow Claude Code untuk mengarahkan pembaca ke PDF gratis, Gumroad, atau konsultasi sesuai intent.
Aturan handoff tim Claude Code: bukti review, permission, rollback, dan jalur revenue
Format handoff Claude Code untuk tim: bukti, permission rule, rollback, PDF gratis, Gumroad, dan konsultasi.