Lộ trình học›Claude Code›Claude Code in Action Mở bản đồ

4.17 · Các hook event khác & Debug hook

Bài viếtTrung cấp25 phút đọc

Bài 4.12–4.16 tập trung PreToolUse và PostToolUse. Đây là 2 event phổ biến nhất.

Sau bài này, bạn sẽ
  1. Liệt kê 7 hook event của Claude Code (không chỉ Pre/PostToolUse)
  2. Chọn event phù hợp cho các use case: notification, stop detection, session lifecycle
  3. Dùng log-only hook để inspect JSON structure mà không cần đoán
  4. Viết hook cho Stop, Notification, SessionStart, UserPromptSubmit
  5. Debug hook với pattern "log first, implement later"

Mở đầu: Pre/Post chưa đủ

Nhưng Claude Code có nhiều event khác — mỗi cái mở use case riêng:

  • Muốn alert khi Claude "stuck" chờ permission? → Notification event
  • Muốn cleanup khi session kết thúc? → Stop event
  • Muốn validate prompt trước khi gửi Claude? → UserPromptSubmit event
  • Muốn init state mỗi khi session start? → SessionStart event

Biết các event này = hook game bạn mở rộng nhiều.

7 hook event của Claude Code

┌──────────────────────────────────────────────────────────┐
│                                                          │
│  LIFECYCLE                                               │
│  ├── SessionStart     — khi Claude Code khởi động        │
│  ├── SessionEnd       — khi Claude Code đóng             │
│  └── UserPromptSubmit — khi bạn submit prompt            │
│                                                          │
│  TOOL                                                    │
│  ├── PreToolUse       — trước tool call                  │
│  └── PostToolUse      — sau tool call                    │
│                                                          │
│  INTERACTION                                             │
│  ├── Notification     — khi Claude cần permission        │
│  └── Stop             — khi Claude finish response       │
│                                                          │
│  SUB-AGENT                                               │
│  ├── SubagentStop     — khi sub-agent (Task) finish      │
│  └── PreCompact       — trước compact operation          │
│                                                          │
└──────────────────────────────────────────────────────────┘

Mô tả từng event

EventTrigger khiUse case điển hình
SessionStartClaude Code start / resumeInit state, sync config, log
SessionEndClaude Code exitCleanup, upload log, commit audit
UserPromptSubmitBạn enter promptSanitize input, pre-filter, rewrite prompt
PreToolUseTrước tool callBlock, validate
PostToolUseSau tool callFormat, test, audit
NotificationClaude xin permission / idle 60sAlert Slack/desktop, auto-approve rule
StopClaude finish 1 responseSummary, log complete task
SubagentStopSubagent finishMerge result, propagate
PreCompactTrước /compact runSave state snapshot

JSON shape khác nhau theo event

Đây là chỗ khó. Mỗi event có shape stdin khác. Và với PreToolUse/PostToolUse, shape còn khác theo tool_name.

Ví dụ 1: PostToolUse trên TodoWrite

{
  "session_id": "9ecf22fa-edf8-4332-ae85-b6d5456eda64",
  "transcript_path": "/Users/you/.claude/transcripts/...",
  "hook_event_name": "PostToolUse",
  "tool_name": "TodoWrite",
  "tool_input": {
    "todos": [
      { "content": "write a readme", "status": "pending", "id": "1" }
    ]
  },
  "tool_response": {
    "oldTodos": [],
    "newTodos": [
      { "content": "write a readme", "status": "pending", "id": "1" }
    ]
  }
}

Ví dụ 2: Stop event

{
  "session_id": "af9f50b6-...",
  "transcript_path": "/Users/you/.claude/transcripts/...",
  "hook_event_name": "Stop",
  "stop_hook_active": false
}

Thấy không? Stop event không có tool_name hay tool_input. Structure hoàn toàn khác.

Ví dụ 3: UserPromptSubmit

{
  "session_id": "...",
  "hook_event_name": "UserPromptSubmit",
  "prompt": "Help me refactor the authentication module"
}

Ví dụ 4: Notification (permission request)

{
  "session_id": "...",
  "hook_event_name": "Notification",
  "notification_type": "permission",
  "tool_name": "Bash",
  "tool_input": { "command": "rm -rf node_modules" }
}

Pattern: Log-first, implement-later

Key insight: Thay vì đoán JSON shape, log trước, viết logic sau.

Cách làm

Bước 1: Tạo hook log-only — match mọi event bạn quan tâm.

.claude/hooks/log.js:

#!/usr/bin/env node
const fs = require('fs');

async function main() {
  const chunks = [];
  for await (const chunk of process.stdin) chunks.push(chunk);
  const raw = Buffer.concat(chunks).toString();
  
  const logLine = `--- ${new Date().toISOString()} ---\n${raw}\n\n`;
  fs.appendFileSync('/tmp/claude-hooks.log', logLine);
  
  process.exit(0);
}

main();

Bước 2: Config log hook cho mọi event:

{
  "hooks": {
    "PreToolUse":       [{ "matcher": "*", "hooks": [{ "type": "command", "command": "node $PWD/.claude/hooks/log.js" }] }],
    "PostToolUse":      [{ "matcher": "*", "hooks": [{ "type": "command", "command": "node $PWD/.claude/hooks/log.js" }] }],
    "Stop":             [{ "hooks": [{ "type": "command", "command": "node $PWD/.claude/hooks/log.js" }] }],
    "Notification":     [{ "hooks": [{ "type": "command", "command": "node $PWD/.claude/hooks/log.js" }] }],
    "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "node $PWD/.claude/hooks/log.js" }] }]
  }
}

Notice: Stop/Notification/UserPromptSubmit không có matcher (không phải tool event).

Bước 3
Chạy Claude session bình thường — làm vài task.
Bước 4
Tail log:
tail -f /tmp/claude-hooks.log

Xem JSON thực tế cho mỗi event. Copy-paste field name vào hook code — không cần đoán.

Bước 5
Viết logic hook dựa trên field đã biết.
Bước 6
Remove log hook khỏi config (hoặc disable), keep hook chính.

Ví dụ hook sử dụng event khác

Hook A: Notification — Desktop alert khi Claude xin permission

Claude chạy async, bạn đang làm việc khác. Claude xin permission → bạn không thấy → Claude idle.

Fix: Hook Notification push desktop notification.

.claude/hooks/notify.js:

#!/usr/bin/env node
const { execSync } = require('child_process');

async function main() {
  const chunks = [];
  for await (const chunk of process.stdin) chunks.push(chunk);
  const input = JSON.parse(Buffer.concat(chunks).toString());
  
  // macOS notification
  const msg = input.tool_name 
    ? `Claude needs permission for: ${input.tool_name}`
    : `Claude idle for 60s`;
  
  execSync(`osascript -e 'display notification "${msg}" with title "Claude Code"'`);
  
  process.exit(0);
}

main().catch(() => process.exit(1));

Config:

"Notification": [
  { "hooks": [{ "type": "command", "command": "node $PWD/.claude/hooks/notify.js" }] }
]

Windows/Linux: thay osascript bằng notify-send (Linux) hoặc powershell (Windows).

Hook B: Stop — Auto-commit sau mỗi session

Claude xong task → tự commit lên branch WIP:

.claude/hooks/auto-commit.js:

#!/usr/bin/env node
const { execSync } = require('child_process');

async function main() {
  const chunks = [];
  for await (const chunk of process.stdin) chunks.push(chunk);
  const input = JSON.parse(Buffer.concat(chunks).toString());
  
  if (input.hook_event_name !== 'Stop') process.exit(0);
  
  // Check có change không
  const status = execSync('git status --porcelain').toString();
  if (!status) process.exit(0);  // Nothing to commit
  
  // Chỉ commit nếu đang trên WIP branch
  const branch = execSync('git branch --show-current').toString().trim();
  if (!branch.startsWith('claude-wip/')) process.exit(0);
  
  try {
    execSync('git add -A', { stdio: 'pipe' });
    execSync(`git commit -m "WIP: Claude session ${input.session_id.slice(0,8)}"`, { stdio: 'pipe' });
    process.exit(0);
  } catch (err) {
    require('fs').appendFileSync('/tmp/auto-commit.log', `${err}\n`);
    process.exit(1);
  }
}

main();

Config:

"Stop": [
  { "hooks": [{ "type": "command", "command": "node $PWD/.claude/hooks/auto-commit.js" }] }
]

Tiện: mỗi session Claude xong, branch WIP có commit "lịch sử". Khi cần rollback, git log --grep="Claude session" → tìm ra.

Hook C: UserPromptSubmit — Sanitize / rewrite prompt

Muốn mọi prompt của team đều được sanitize (xóa PII, correct typo):

.claude/hooks/sanitize-prompt.js:

#!/usr/bin/env node

async function main() {
  const chunks = [];
  for await (const chunk of process.stdin) chunks.push(chunk);
  const input = JSON.parse(Buffer.concat(chunks).toString());
  
  let prompt = input.prompt || '';
  
  // Strip potential PII patterns
  prompt = prompt.replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN]');
  prompt = prompt.replace(/\b[\w.-]+@[\w.-]+\.\w+\b/g, '[EMAIL]');
  
  // If changed, emit warning to log
  if (prompt !== input.prompt) {
    require('fs').appendFileSync('/tmp/sanitized.log', 
      `Original: ${input.prompt}\nSanitized: ${prompt}\n---\n`);
  }
  
  // Log only for now, don't alter prompt
  // (Actual prompt rewriting requires different mechanism)
  process.exit(0);
}

main();

Note: Hiện tại UserPromptSubmit chủ yếu log/validate, không rewrite prompt dễ dàng. Xem docs mới nhất cho capability update.

Hook D: SessionStart — Auto-sync config

Mỗi khi Claude session mới, pull latest team config:

.claude/hooks/session-init.js:

#!/usr/bin/env node
const { execSync } = require('child_process');

async function main() {
  try {
    // Pull latest CLAUDE.md if team has shared repo
    execSync('git pull --no-edit origin main -- CLAUDE.md .claude/', { 
      stdio: 'pipe',
      cwd: process.cwd()
    });
  } catch {
    // Fail silently — offline or no conflict
  }
  process.exit(0);
}

main();

Config:

"SessionStart": [
  { "hooks": [{ "type": "command", "command": "node $PWD/.claude/hooks/session-init.js" }] }
]

Đảm bảo mỗi session, config mới nhất từ team.

Hook E: PreCompact — Snapshot trước compact

Trước khi /compact xóa context, backup:

#!/usr/bin/env node
const fs = require('fs');

async function main() {
  const chunks = [];
  for await (const chunk of process.stdin) chunks.push(chunk);
  const input = JSON.parse(Buffer.concat(chunks).toString());
  
  // Copy transcript trước khi compact
  const transcript = fs.readFileSync(input.transcript_path, 'utf8');
  const backup = `/tmp/claude-backup-${Date.now()}.log`;
  fs.writeFileSync(backup, transcript);
  console.error(`Transcript backed up to ${backup}`);
  
  process.exit(0);
}

main();

Recover lại conversation nếu compact làm mất context quan trọng.

Ví dụ thực chiến: Team dùng 5 hook event

Setup

Team 10 dev. Setup hook strategy:

1. PreToolUse:   Block .env, dangerous bash      [security]
2. PostToolUse:  Auto-format + typecheck         [quality]
3. Notification: Desktop alert khi xin permission [UX]
4. Stop:         Auto-commit WIP branch          [safety]
5. SessionStart: Git pull config                 [consistency]

Kết quả sau 3 tháng

  • Security: zero .env leak incident
  • Quality: PR with TS error → 0
  • UX: dev respond permission ngay (thay vì miss → Claude idle)
  • Safety: rollback possible mọi session via git log
  • Consistency: 100% dev cùng config mới nhất

Case studies theo ngành

Customer success — Prompt analytics

Setup: UserPromptSubmit hook log prompt vào analytics DB → insight dashboard cho manager.

  • Kết quả: Biết team hay hỏi gì → cải tiến CLAUDE.md target pain points.

Health tech — Session audit cho SOC2

Setup: SessionStart + SessionEnd hook ghi session metadata (user, start/end time, files touched) vào append-only audit log.

  • Kết quả: Evidence complete cho audit, pass SOC2 + HIPAA.

Game dev — Auto-screenshot khi edit asset

Setup: PostToolUse trên edit assets/*.png → hook tự commit + screenshot visual diff cho QA review.

  • Kết quả: Asset change tracking automated, QA review 5x nhanh.

Platform team — Notification Slack

Setup: Notification hook → post vào Slack channel cá nhân: "Claude cần permission cho X".

  • Kết quả: Dev không miss permission prompt, saved 30 phút/tuần/dev.

EdTech — Teacher mode

Setup: SessionStart load student context (assignment, deadline), PostToolUse log cho teacher review.

  • Kết quả: Teacher visibility vào student AI usage, detect cheating pattern.

Anti-patterns

Hook log-all chạy mãi không remove

Biểu hiện: Dev debug log hook → quên remove → file /tmp/ phình to 100GB sau 2 tuần.

Cách đúng: Remove log-only hook sau khi inspect xong. Hoặc rotate log file.

Notification hook spam

Biểu hiện: Notification mọi permission request → 50 notification/giờ → annoying.

Cách đúng: Notification chỉ cho tool nguy hiểm (Bash destructive), không cho Read/Edit thường.

SessionStart quá chậm

Biểu hiện: Git pull chậm → session start block 30s.

Cách đúng: Timeout sớm, async, hoặc skip nếu offline.

Hook modify transcript

Biểu hiện: Dev tự viết hook modify transcript_path để ẩn info.

Rủi ro
Audit log không còn trustworthy.
Cách đúng
Không tamper với transcript. Nếu cần mask, log riêng vào stream khác.

Quên xử lý fail gracefully

Biểu hiện: Stop hook crash → Claude Code không thể exit normally.

Cách đúng: Try/catch mọi thứ, exit 0 hoặc 1, không 2 (2 block ảnh hưởng behavior).

Mẹo nâng cao

Mẹo 1: Hook orchestrator

Nếu có nhiều hook, tạo 1 wrapper script route:

#!/usr/bin/env node
const input = JSON.parse(await readStdin());

switch (input.hook_event_name) {
  case 'PreToolUse':     return require('./hooks/pre-tool')(input);
  case 'PostToolUse':    return require('./hooks/post-tool')(input);
  case 'Stop':           return require('./hooks/stop')(input);
  // ...
}

Config chỉ có 1 command point → wrapper. Dễ maintain.

Mẹo 2: Conditional hook bằng env

Dev có thể toggle:

CLAUDE_HOOKS=off claude    # disable all
CLAUDE_HOOKS=strict claude # all enabled

Mẹo 3: Hook với telemetry

Mỗi hook log run time + outcome vào metrics DB. Dashboard show:

  • Hook X trigger N lần/ngày
  • Avg latency
  • Block rate
  • Error rate

Dữ liệu lý giải quyết tối ưu hóa.

Mẹo 4: Share hook qua npm

Team lớn: @acme/claude-hooks package chứa mọi hook. Dev npm install, config minimal.

Áp dụng ngay

Bài tập 1: Log-first debugging (15 phút)

Bước 1
Tạo log hook universal như trên.
Bước 2
Config cho TẤT CẢ event.
Bước 3
Chạy Claude session làm task đa dạng (edit, bash, grep, submit prompt, exit).
Bước 4
Check /tmp/claude-hooks.log. Trả lời:
  • Bao nhiêu unique hook_event_name thấy trong log? ____
  • Shape của Stop event khác PreToolUse như thế nào? ____
  • UserPromptSubmit có field gì? ____

Bước 5: Remove log hook sau khi xong.

Bài tập 2 (optional, 20 phút): Chọn 1 event mới cho project

Brainstorm: event nào sẽ giúp team/project bạn? Một ý tưởng mỗi dev:

  • Notification → Slack alert cho team lead?
  • Stop → Auto-generate summary + commit?
  • SessionStart → Pull config + run health check?

Implement 1 hook nhỏ. Test. Commit vào .claude/hooks/.

Tóm tắt bài học

Những điều cốt lõi
  • 7 hook event — lifecycle, tool, interaction, sub-agent. Pre/Post không phải tất cả.
  • JSON shape khác theo event + tool — không đoán, log trước.
  • Log-only hook = debug tool #1 — setup 2 phút, tiết kiệm nhiều phút đoán.
  • Event mở use case mới — notification, auto-commit, session sync, prompt analytics.
  • Graceful fail — hook không crash Claude Code; luôn exit code phù hợp.

Tài liệu tham khảo

Bạn đã hoàn thành bài này chưa?Tiến độ lưu trên trình duyệt của bạn.