Các hook event khác & Debug hook

5 — HooksTrung cấp25 phút

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

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

7 hook event của Claude Code

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
┌──────────────────────────────────────────────────────────┐
│                                                          │
│  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          │
│                                                          │
└──────────────────────────────────────────────────────────┘

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

Ví dụ 2: Stop event

{
  "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

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": "af9f50b6-...",
  "transcript_path": "/Users/you/.claude/transcripts/...",
  "hook_event_name": "Stop",
  "stop_hook_active": false
}

Ví dụ 3: UserPromptSubmit

Ví dụ 4: Notification (permission request)

{
  "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:

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

#!/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();

Cách làm

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:

{
  "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" }] }]
  }
}

Pattern: Log-first, implement-later (tiếp)

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.

tail -f /tmp/claude-hooks.log

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:

Config:

#!/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));

Hook A: Notification — Desktop alert khi Claude xin permission

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:

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

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

Config:

#!/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();

Ví dụ hook sử dụng event khác (tiếp)

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:

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

Hook C: UserPromptSubmit — Sanitize / rewrite prompt

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

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();

Hook D: SessionStart — Auto-sync config

Config:

#!/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();

Ví dụ hook sử dụng event khác (tiếp)

Đả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:

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

Hook E: PreCompact — Snapshot trước compact

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

#!/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();

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

Setup

Team 10 dev. Setup hook strategy:

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
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]

Case studies theo ngành

💼 Customer success — Prompt analytics

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

🏥 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.

🎮 Game dev — Auto-screenshot khi edit asset

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

🛠️ Platform team — Notification Slack

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

🎓 EdTech — Teacher mode

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

  • Kết quả: Biết team hay hỏi gì → cải tiến CLAUDE.md target pain points.
  • Kết quả: Evidence complete cho audit, pass SOC2 + HIPAA.
  • Kết quả: Asset change tracking automated, QA review 5x nhanh.
  • Kết quả: Dev không miss permission prompt, saved 30 phút/tuần/dev.
  • 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:

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

Mẹo 2: Conditional hook bằng env

Dev có thể toggle:

#!/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);
  // ...
}

Mẹo 2: Conditional hook bằng env

Mẹo 3: Hook với telemetry

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

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.

  • Hook X trigger N lần/ngày
  • Avg latency
  • Block rate
  • Error rate
CLAUDE_HOOKS=off claude    # disable all
CLAUDE_HOOKS=strict claude # all enabled

Á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:

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:

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

  • 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ì? ____
  • Notification → Slack alert cho team lead?
  • Stop → Auto-generate summary + commit?
  • SessionStart → Pull config + run health check?

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

🎯 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
  • Claude Code hooks full reference
  • Hook event schema
  • Bài 4.12 — Giới thiệu hook
  • Bài 4.18 — SDK
Nội dung này có hữu ích không?