Bài 4.12–4.16 tập trung PreToolUse và PostToolUse. Đây là 2 event phổ biến nhất.
- 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
| Event | Trigger khi | Use case điển hình |
|---|---|---|
| SessionStart | Claude Code start / resume | Init state, sync config, log |
| SessionEnd | Claude Code exit | Cleanup, upload log, commit audit |
| UserPromptSubmit | Bạn enter prompt | Sanitize input, pre-filter, rewrite prompt |
| PreToolUse | Trước tool call | Block, validate |
| PostToolUse | Sau tool call | Format, test, audit |
| Notification | Claude xin permission / idle 60s | Alert Slack/desktop, auto-approve rule |
| Stop | Claude finish 1 response | Summary, log complete task |
| SubagentStop | Subagent finish | Merge result, propagate |
| PreCompact | Trước /compact run | Save 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.logVí 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.
- Claude Code hooks full reference
- Hook event schema
- Bài 4.12 — Giới thiệu hook
- Bài 4.18 — SDK