Thông Báo Discord Khi Phase Hoàn Thành
Nhận thông báo trên Discord khi hoàn thành phase hoặc cần bạn approve
Bạn bắt đầu một session Claude Code nhiều phase. Phase 1 chạy. Bạn đi pha cà phê. Check Slack. Bị kéo vào họp. Hai tiếng sau quay lại. Claude đã xong Phase 1 từ 8 phút đầu và ngồi chờ bạn approve từ đó tới giờ.
Vấn Đề
- • Session Claude Code dài với plan nhiều phase cần user approve giữa các phase
- • Không có notification tích hợp nên Claude chờ trong im lặng
- • Chuyển đổi ngữ cảnh làm giảm năng suất - hoặc bạn phải ngồi canh Claude code xong phase rồi approve, hoặc bạn đi làm việc khác rồi quên quay lại bắt nó chờ bạn approve hoài
- → Giải pháp: custom hook ping bạn trên Discord khi Claude cần attention
Giải Pháp
Tạo một custom hook để:
- Detect phase complete, approval request, và finalization bằng pattern matching
- Gửi Discord notification để bạn yên tâm làm việc khác
- Cross-platform (macOS, Linux, Windows) dùng Node.js
httpsmodule - Đặt trong
~/.claude/custom/hooks/nên không bị mất khi update ClaudeKit - Chỉ kích hoạt khi có từ khóa quan trọng (không spam mỗi lần stop)
Detection Patterns
| Loại | Emoji | Màu | Trigger Phrases |
|---|---|---|---|
| /plan Complete | 📋 | #3498db | "ExitPlanMode", "exiting plan mode" |
| /code Step 5 | ⏸ | #f1c40f | "⏸ Step 5", "WAITING for user approval" |
| /code Phase Complete | ✅ | #2ecc71 | "✓ Step 6: Finalize", "Git committed", "Phase workflow finished" |
Các Bước Setup
Tạo Discord Webhook
- Mở Discord và vào server của bạn
- Right-click a channel → Edit Channel → Integrations → Webhooks → New Webhook
- Đặt tên webhook tuỳ ý (ví dụ: "Claude Code")
- Copy webhook URL (giữ bí mật, đừng cho người khác biết!)
Tạo Thư Mục Custom Hooks
macOS / Linux:
mkdir -p ~/.claude/custom/hooks New-Item -ItemType Directory -Force -Path $env:USERPROFILE\.claude\custom\hooks Thư mục custom/ không bị ClaudeKit update đè, hooks của bạn sẽ được giữ mỗi khi CK update.
Tạo Notification Script
Tạo file: ~/.claude/custom/hooks/discord-phase-notify.cjs
#!/usr/bin/env node
/**
* Discord Phase Notification Hook (Minimal Version)
*
* Only notifies on high-signal events:
* 1. /plan command - ExitPlanMode when planning phase completes
* 2. /code command - Step 5 blocking gate waiting for user approval
* 3. /code command - Step 6 finalize when phase workflow finishes
*
* Location: ~/.claude/custom/hooks/ (survives CK updates)
* Events: Stop (main session only, not SubagentStop to reduce noise)
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const os = require('os');
// Strict detection patterns - only high-signal events
const PATTERNS = {
// /plan command: Plan ready for approval (ExitPlanMode tool called)
planReady: [
/ExitPlanMode/,
/exiting\s+plan\s+mode/i,
],
// /code command: Step 5 blocking gate - waiting for user approval
codeApproval: [
/⏸\s*Step\s*5/,
/Step\s*5.*WAITING.*user\s*approval/i,
/WAITING\s+for\s+user\s+approval/i,
],
// /code command: Step 6 finalize - phase workflow finished
codeFinalize: [
/✓\s*Step\s*6.*Finalize/,
/Step\s*6.*Git\s*committed/i,
/Phase\s+workflow\s+finished/i,
],
};
// Load environment variables (project scope first, then global)
function loadEnv() {
const cwd = process.cwd();
const envPaths = [
// Project scope first
path.join(cwd, '.claude', '.env'),
path.join(cwd, '.claude', 'hooks', '.env'),
// Global scope fallback
path.join(os.homedir(), '.claude', 'custom', 'hooks', '.env'),
path.join(os.homedir(), '.claude', 'hooks', '.env'),
path.join(os.homedir(), '.claude', '.env'),
];
for (const envPath of envPaths) {
if (fs.existsSync(envPath)) {
const content = fs.readFileSync(envPath, 'utf-8');
content.split('\n').forEach(line => {
const match = line.match(/^([^=]+)=(.*)$/);
if (match && !process.env[match[1]]) {
process.env[match[1]] = match[2].replace(/^["']|["']$/g, '');
}
});
}
}
}
// Send Discord notification (cross-platform using native https)
function sendDiscord(title, message, color = 5763719) {
return new Promise((resolve) => {
const webhookUrl = process.env.DISCORD_WEBHOOK_URL;
if (!webhookUrl) {
console.error('DISCORD_WEBHOOK_URL not set - skipping notification');
resolve(false);
return;
}
const cwd = process.cwd();
const project = path.basename(cwd);
const timestamp = new Date().toISOString();
const localTime = new Date().toLocaleTimeString('en-US', { hour12: false });
const payload = JSON.stringify({
embeds: [{
title,
description: message,
color,
timestamp,
footer: { text: `${project} • ${cwd}` },
fields: [
{ name: '⏰ Time', value: localTime, inline: true },
{ name: '📂 Project', value: project, inline: true },
],
}],
});
try {
const url = new URL(webhookUrl);
const options = {
hostname: url.hostname,
port: 443,
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
},
timeout: 10000,
};
const req = https.request(options, (res) => {
resolve(res.statusCode >= 200 && res.statusCode < 300);
});
req.on('error', (e) => {
console.error(`Discord notification failed: ${e.message}`);
resolve(false);
});
req.on('timeout', () => {
req.destroy();
console.error('Discord notification timed out');
resolve(false);
});
req.write(payload);
req.end();
} catch (e) {
console.error(`Discord notification failed: ${e.message}`);
resolve(false);
}
});
}
// Detect notification type from text - strict matching
function detectType(text) {
if (!text) return null;
// /plan command: ExitPlanMode (plan ready for review)
for (const pattern of PATTERNS.planReady) {
if (pattern.test(text)) {
return { type: 'planReady', title: '📋 /plan Complete', color: 3447003 }; // Blue
}
}
// /code command: Step 6 finalize (phase workflow finished) - check before Step 5
for (const pattern of PATTERNS.codeFinalize) {
if (pattern.test(text)) {
return { type: 'codeFinalize', title: '✅ /code Phase Complete', color: 5763719 }; // Green
}
}
// /code command: Step 5 blocking gate
for (const pattern of PATTERNS.codeApproval) {
if (pattern.test(text)) {
return { type: 'codeApproval', title: '⏸ /code Step 5: Approval Needed', color: 15844367 }; // Yellow
}
}
return null;
}
// Extract relevant context from payload
function extractContext(payload) {
const parts = [];
if (payload.stop_reason) {
parts.push(payload.stop_reason.slice(0, 300));
}
return parts.join('\n') || 'Claude needs your attention';
}
// Main execution
async function main() {
try {
loadEnv();
const stdin = fs.readFileSync(0, 'utf-8').trim();
if (!stdin) process.exit(0);
const payload = JSON.parse(stdin);
const textParts = [
payload.stop_reason,
payload.result,
payload.output,
payload.message,
].filter(Boolean);
if (payload.transcript_path && fs.existsSync(payload.transcript_path)) {
try {
const transcript = fs.readFileSync(payload.transcript_path, 'utf-8');
textParts.push(transcript.slice(-3000));
} catch (e) {
// Ignore
}
}
const fullText = textParts.join(' ');
const detection = detectType(fullText);
if (detection) {
const context = extractContext(payload);
const sent = await sendDiscord(detection.title, context, detection.color);
if (sent) {
console.log(`Discord: ${detection.title}`);
}
}
process.exit(0);
} catch (error) {
console.error(`Hook error: ${error.message}`);
process.exit(0);
}
}
main(); Chmod (macOS/Linux):
chmod +x ~/.claude/custom/hooks/discord-phase-notify.cjs
Save the script to %USERPROFILE%\.claude\custom\hooks\discord-phase-notify.cjs
Note: Windows không cần chmod - Node.js script chạy trực tiếp.
Config Claude Code Hooks
Thêm vào ~/.claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node $HOME/.claude/custom/hooks/discord-phase-notify.cjs"
}
]
}
]
}
} Windows: Thay $HOME bằng full path như C:\Users\YourName\.claude\custom\hooks\discord-phase-notify.cjs
Set Webhook URL
Thêm vào ~/.claude/.env:
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN Test
/plan Complete
Trigger khi /plan command hoàn thành (ExitPlanMode):
macOS / Linux:
echo '{"stop_reason": "ExitPlanMode - plan is ready for your review"}' | node ~/.claude/custom/hooks/discord-phase-notify.cjs '{"stop_reason": "ExitPlanMode - plan is ready for your review"}' | node $env:USERPROFILE\.claude\custom\hooks\discord-phase-notify.cjs /code Step 5
Trigger khi /code command đến Step 5 blocking gate:
macOS / Linux:
echo '{"stop_reason": "⏸ Step 5: WAITING for user approval"}' | node ~/.claude/custom/hooks/discord-phase-notify.cjs '{"stop_reason": "⏸ Step 5: WAITING for user approval"}' | node $env:USERPROFILE\.claude\custom\hooks\discord-phase-notify.cjs /code Step 6
Trigger khi /code command hoàn thành Step 6 finalization:
macOS / Linux:
echo '{"stop_reason": "✓ Step 6: Finalize - Status updated - Git committed"}' | node ~/.claude/custom/hooks/discord-phase-notify.cjs '{"stop_reason": "✓ Step 6: Finalize - Status updated - Git committed"}' | node $env:USERPROFILE\.claude\custom\hooks\discord-phase-notify.cjs Mỗi test sẽ trigger notification khác nhau với emoji và màu tương ứng.
Hoặc: Để LLM Setup Hết
Copy prompt này và để Claude Code setup cho bạn:
Tuỳ Chỉnh Cho Các Event Khác
Hook này lắng nghe event Stop. Bạn có thể mở rộng hoặc thêm các hook event khác:
Các Hook Event Có Sẵn
| Event | Description |
|---|---|
| PreToolUse | Trước khi tool được thực thi |
| PostToolUse | Sau khi tool hoàn thành |
| Stop | Khi Claude dừng (session chính) |
| SubagentStop | Khi subagent hoàn thành (hay bị trigger liên tục) |
| Notification | Khi Claude gửi notification |
Thêm Pattern Tuỳ Chỉnh
Chỉnh sửa object PATTERNS trong script:
// Thêm pattern tuỳ chỉnh
const PATTERNS = {
// Pattern có sẵn...
planReady: [/ExitPlanMode/, /exiting\s+plan\s+mode/i],
codeApproval: [/⏸\s*Step\s*5/, /WAITING\s+for\s+user\s+approval/i],
codeFinalize: [/✓\s*Step\s*6.*Finalize/, /Phase\s+workflow\s+finished/i],
// Tuỳ chỉnh: Thông báo khi test hoàn thành
testsComplete: [
/All tests passed/i,
/\d+ passed, 0 failed/,
],
// Tuỳ chỉnh: Thông báo khi có lỗi
errorOccurred: [
/Error:/i,
/Failed to/i,
/Exception/i,
],
};
// Cập nhật detectType() để xử lý pattern mới
function detectType(text) {
// ... các check có sẵn ...
for (const pattern of PATTERNS.testsComplete) {
if (pattern.test(text)) {
return { type: 'testsComplete', title: '✅ Tests Passed', color: 5763719 }; // Green
}
}
for (const pattern of PATTERNS.errorOccurred) {
if (pattern.test(text)) {
return { type: 'error', title: '❌ Error Detected', color: 15548997 }; // Red
}
}
return null;
} Lắng Nghe Nhiều Event
Thêm nhiều event trong settings.json:
{
"hooks": {
"Stop": [{ "hooks": [{ "type": "command", "command": "node $HOME/.claude/custom/hooks/discord-phase-notify.cjs" }] }],
"SubagentStop": [{ "hooks": [{ "type": "command", "command": "node $HOME/.claude/custom/hooks/discord-phase-notify.cjs" }] }],
"Notification": [{ "hooks": [{ "type": "command", "command": "node $HOME/.claude/custom/hooks/discord-phase-notify.cjs" }] }]
}
} SubagentStop trigger thường xuyên - dùng pattern cụ thể để lọc bớt noise.
Màu embed Discord là giá trị decimal. Phổ biến: Green=5763719, Red=15548997, Blue=3447003, Yellow=15844367, Purple=10181046.