Mục tiêu học tập
Sau bài này, bạn sẽ có thể:- ✅ Giải thích hook là gì và tại sao nó deterministic (tất định) trong khi CLAUDE.md và skill thì probabilistic (xác suất)
- ✅ Nắm rõ 5 lifecycle events: PreToolUse, PostToolUse, UserPromptSubmit, Stop, Notification — và khi nào dùng cái nào
- ✅ Cấu hình hooks đúng cách trong settings.json với matcher, command, và script
- ✅ Dùng exit codes (0 / 2) để allow hoặc block tool call từ PreToolUse hook
- ✅ Share hooks với cả team bằng cách commit settings.json và script vào repo
Mở đầu: “Claude quên” không còn là lý do hợp lý nữa
Một dev backend ở startup fintech đã viết vào CLAUDE.md:“Sau mỗi lần edit file TypeScript, hãy chạy Prettier để format code.”Đơn giản, rõ ràng. Trong 2 tuần đầu, Claude làm đúng khoảng 80% thời gian. Đủ tốt để bỏ qua. Cho đến buổi chiều thứ Sáu trước ngày release. CI pipeline báo đỏ: 47 file lỗi format. Dev phải ngồi lại chạy Prettier tay toàn bộ thư mục src/. Mất 40 phút. Sprint bị trễ. Vấn đề không phải Claude “lười”. Vấn đề là CLAUDE.md là probabilistic — model đọc rồi quyết định có thực hiện hay không, dựa trên ngữ cảnh hiện tại, độ dài cuộc hội thoại, và hàng chục yếu tố khác. 80% tốt là một con số tốt trong ML. Nhưng trong production workflow, 80% nghĩa là 1 trong 5 lần bạn sẽ bị bất ngờ. Hook giải quyết chính xác vấn đề này. Khi bạn đặt logic vào hook, Claude không còn là người quyết định nữa. Hệ thống Claude Code chạy script của bạn tại event được chỉ định — mà không cần model “nhớ” hay “quyết định”. 100% mỗi lần, không ngoại lệ.
“If something needs to happen every time without fail, don’t put it in a prompt. Put it in a hook.” — Hooks documentation, AnthropicĐây là bài học bạn sẽ nhớ mãi sau khi đọc xong bài này.
Hook là gì?
Hook là một script chạy tự động tại các điểm cụ thể trong lifecycle của Claude Code. Bạn cấu hình hook trong settings.json — khai báo: event nào, tool nào (matcher), và command nào cần chạy. Điểm cốt lõi phân biệt hook với mọi cơ chế khác:| Cơ chế | Tính chất | Ai quyết định chạy? |
|---|---|---|
| Hook | Deterministic — luôn chạy | Hệ thống Claude Code (không phải model) |
| CLAUDE.md | Probabilistic | Model đọc, model quyết định |
| Skill | Probabilistic | Model nhận request, model quyết định invoke |
| MCP | Available-when-needed | Model quyết định gọi tool |
Lifecycle của Claude Code — nơi hooks can thiệp
5 Hook Events
| Event | Chạy khi nào | Có thể block? | Typical use case | Cần matcher? |
|---|---|---|---|---|
| PreToolUse | Trước khi tool call thực thi | Có (exit 2) | Block write vào prod file, block rm -rf, validate input | Khuyến nghị |
| PostToolUse | Sau khi tool call hoàn thành | Không | Auto-format code, chạy lint, ghi audit log, trigger test | Khuyến nghị |
| UserPromptSubmit | Khi user submit prompt, trước khi Claude xử lý | Không trực tiếp | Log prompt, inject thêm context, validate prompt format | Không bắt buộc |
| Stop | Khi Claude hoàn thành response | Không | Gửi Slack notification, cleanup temp files, trigger deploy | Không bắt buộc |
| Notification | Khi Claude gửi notification | Không | Custom notification routing, macOS say, Slack ping | Không bắt buộc |
Anatomy của hook config
Hook được cấu hình trong.claude/settings.json (project-level, có thể commit vào repo) hoặc ~/.claude/settings.json (user-level, áp dụng mọi project).
Cấu trúc JSON
Giải thích từng thành phần
- Event key (PostToolUse, PreToolUse, v.v.) — lifecycle event bạn muốn hook vào
- matcher — regex string match với tên tool.
"Edit|MultiEdit|Write"nghĩa là hook chỉ chạy khi Claude dùng một trong 3 tools này. Để trống""nghĩa là match mọi tool call - type — hiện tại chỉ có
"command"(chạy shell command) - command — lệnh shell hoặc đường dẫn tới script. Dùng
$CLAUDE_PROJECT_DIRthay vì hardcode absolute path
Script nhận gì từ Claude Code?
Script của bạn nhận JSON qua stdin chứa thông tin về event:Exit Codes và Behavior
| Exit code | Behavior | Use case | Ví dụ |
|---|---|---|---|
| 0 | Proceed normally — tool call được allow hoặc PostToolUse tiếp tục | Mọi trường hợp bình thường | Script chạy Prettier thành công |
| 2 | Block tool call (chỉ valid cho PreToolUse). stderr message được feed back cho Claude như feedback | Enforce hard rules | Block write vào infra/prod/* |
| Khác (1, 3, …) | Non-blocking error — hiển thị cho user nhưng không dừng Claude | Script lỗi nhưng không muốn block | Prettier không tìm thấy, bỏ qua |
Ví dụ thực chiến: Auto-format hook
Đây là hook phổ biến nhất — auto-format file sau mỗi lần Claude edit.Bước 1: Tạo script
Tạo file.claude/hooks/format-on-edit.sh:
Bước 2: Cấp quyền thực thi
Bước 3: Thêm vào settings.json
Bước 4: Commit vào repo
Bước 5: Test
Blocking với PreToolUse
PreToolUse hook là cơ chế enforce “hard rules” — những điều phải được đảm bảo, không phải chỉ “đề xuất”.Script nhận gì?
Ví dụ: Block lệnh Bash nguy hiểm
Tạo.claude/hooks/block-dangerous-commands.sh:
rm -rf /tmp/cache:
Hook vs Skill vs CLAUDE.md vs MCP
Đây là bảng so sánh quan trọng nhất trong bài — quyết định khi nào dùng cơ chế nào:| Tiêu chí | Hook | Skill | CLAUDE.md | MCP |
|---|---|---|---|---|
| Tính chất | Deterministic — luôn chạy | Probabilistic — model quyết định | Probabilistic — model đọc, có thể quên | Available-when-needed |
| Trigger | Event trong lifecycle | Model match request với skill name | Load lúc session start, model đọc | Model quyết định gọi tool |
| Visibility | Script ngoài context | Load vào context khi match | Luôn trong context | Tool definition trong context |
| Best for | Guarantees — phải xảy ra 100% | Patterns — workflow tái sử dụng | Project-wide rules — conventions, commands | External integrations — databases, APIs |
| Ví dụ | Auto-format mỗi edit | /commit với chuẩn commit message | ”Dùng pnpm, không npm” | GitHub, Linear, Slack |
| Overhead | Chạy script ngoài | Load markdown vào context | Luôn có trong context | Tool defs trong context |
- Nếu bạn cần đảm bảo điều gì đó xảy ra → Hook
- Nếu bạn muốn Claude biết cách làm gì đó khi bạn hỏi → Skill
- Nếu bạn muốn Claude luôn nhớ context chung của project → CLAUDE.md
- Nếu bạn cần Claude truy cập hệ thống ngoài → MCP
Case studies theo role
Backend Engineer: Lint guarantee
Mỗi lần Claude edit bất kỳ file.ts, PostToolUse hook auto-chạy eslint --fix. Không bao giờ có CI lint failure nữa — dù session dài 4 tiếng hay Claude đang cuối context.
DevOps / Security Engineer: Prod file protection
PreToolUse hook block mọi write vàoinfra/prod/*. Claude chỉ được phép thao tác infra/staging/*. Block + stderr message rõ ràng giúp Claude tự chọn đường dẫn staging, không cần dev can thiệp.
Engineering Team: Async notification
Stop hook gửi Slack message khi Claude hoàn thành task. Team có thể giao Claude task dài (30+ phút), làm việc khác, nhận Slack ping khi xong. Không cần ngồi nhìn màn hình chờ.Compliance team: Audit log bắt buộc
PreToolUse hook log mọi Bash command vào file audit với timestamp, user, command. Không có exception. Đây là regulatory requirement mà hook đảm bảo 100% — không thể “quên” như khi viết trong CLAUDE.md.Open Source Maintainer: Test trước commit
PostToolUse hook chạy test suite sau mỗi lần Claude edit file source (không phải test file). Nếu test fail, script log ra stderr — Claude biết có regression và tự fix trước khi tiếp tục.Solo founder: macOS notification
Notification hook chạy say “Claude xong rồi” (macOS text-to-speech). Bạn có thể rời bàn phím, về máy sẽ nghe thông báo khi Claude hoàn thành task background.Anti-patterns
Script chạy quá lâu (>10 giây)
Hook là synchronous trong lifecycle. Nếu script mất 30 giây (ví dụ chạy full test suite mỗi edit), mỗi tool call của Claude sẽ bị block 30 giây. UX cực kỳ tệ. Giải pháp: chạy test nhanh, hoặc dùng background process (&) với Stop hook thay vì PostToolUse.
PreToolUse không xử lý input rỗng
Nếu tool call không có field bạn expect (ví dụ Bash không có command khi là subshell), script crash → false-positive block. Luôn handle gracefully:|| echo "" và check empty trước khi process.
Hardcode absolute path trong command
CLAUDE_PROJECT_DIR là env var Claude Code inject vào khi chạy hook — luôn trỏ đến root của project hiện tại.
Không commit settings.json và script vào repo
Nếu chỉ có bạn có hook, team sẽ commit code không format, không lint. Mất đi toàn bộ lợi ích “deterministic for the whole team”. Hook chỉ thực sự mạnh khi toàn team có cùng config.Hook chứa secret / API key
Script được commit vào repo. Nếu script hardcodeSLACK_TOKEN=xoxb-..., bạn vừa leak secret. Dùng environment variable: $SLACK_WEBHOOK_URL và set trong môi trường local / CI secrets.
Dùng hook cho việc nên là Skill
Hook là cho guarantees. Nếu bạn viết hook để “nhắc Claude viết commit message đúng chuẩn”, đó là việc của Skill hoặc CLAUDE.md — không cần guarantee mỗi tool call. Hook không cần thiết = overhead vô nghĩa.Matcher quá rộng (no matcher)
Mẹo nâng cao
Dùng CLAUDE_PROJECT_DIR cho mọi path reference
Combine hooks: PreToolUse → PostToolUse → Stop pipeline
Ba hooks phối hợp tạo một pipeline hoàn chỉnh: validate trước, cleanup sau, notify khi xong.Idempotent scripts — chạy nhiều lần không có side effect
Prettier, gofmt, ruff đều idempotent (chạy nhiều lần ra cùng kết quả). Tuy nhiên nếu script của bạn append vào file log, đảm bảo format đủ thông tin để avoid duplicate entries gây nhầm lẫn.Test hook isolation trước khi enable
Conditional logic trong script (không chỉ dựa vào matcher)
Matcher chỉ match tên tool. Script có thể implement logic phức tạp hơn:Debug: comment tạm matcher để isolate hook behavior
Khi debug Claude behavior, bạn nghi hook đang can thiệp. Cách nhanh nhất: đổi matcher thành một string không bao giờ match ("DISABLED_Edit|MultiEdit"). Claude behavior trở về “thuần” — so sánh để xác nhận hook là culprit.
Multiple matchers trong 1 entry
Áp dụng ngay
Bài tập 1: Auto-format hook (~25 phút)
Tạo PostToolUse hook auto-format cho project của bạn. Bước 1: Xác định formatter bạn đang dùng (Prettier, eslint, ruff, gofmt, v.v.) Bước 2: Tạo.claude/hooks/format-on-edit.sh (xem script ở phần trên, adapt cho stack của bạn)
Bước 3: Thêm vào .claude/settings.json:
Bài tập 2: PreToolUse blocking hook (~20 phút)
Tạo hook block 1 dangerous pattern trong project của bạn. Chọn 1 trong các scenario:- Block rm -rf trong Bash commands
- Block write vào .env files
- Block edit files trong infra/prod/
- Block git push —force
.claude/hooks/block-dangerous.sh với logic check và exit 2
Bước 2: Test reject case: tạo mock input với command nguy hiểm, verify script exit 2 với đúng stderr message
Bước 3: Test allow case: mock input với command bình thường, verify script exit 0
Bước 4: Enable trong settings.json, test với Claude thực tế
Bonus: Thử nhờ Claude làm action bị block → xem Claude tự adjust như thế nào dựa vào stderr feedback
Tóm tắt
Hook là cơ chế duy nhất trong Claude Code mà hành vi là deterministic — không phụ thuộc vào model “nhớ” hay “quyết định”. Đây là công cụ để bạn enforce hard rules, không phải đề xuất. 5 takeaways cốt lõi:- Hook chạy tại lifecycle events — script thông thường, không có technology mới để học
- PostToolUse cho guarantees sau action: format, lint, log, test — chạy mỗi lần không trừ
- PreToolUse cho blocking: exit 2 + stderr message → Claude tự điều chỉnh
- Stop / Notification cho async notifications — để làm việc khác, nhận ping khi Claude xong
- Commit settings.json + script vào repo → toàn team có cùng hook, không ai bị “thiếu”
“If something needs to happen every time without fail, don’t put it in a prompt. Put it in a hook.”
Bài tiếp theo
Bạn đã nắm toàn bộ hệ sinh thái mở rộng của Claude Code: CLAUDE.md, Skills, MCP, và Hooks. Bài 2.12 sẽ là quiz tổng hợp — kiểm tra lại toàn bộ kiến thức từ Bài 2.1 đến 2.11, với câu hỏi thực tế về cách chọn đúng công cụ trong từng tình huống. Cross-reference: Bài 2.4 (hook trong EPCC workflow) — Bài 2.6 (hook auto-run lint trước commit) — Bài 2.7 (hook vs CLAUDE.md khi nào dùng cái nào) — Bài 2.9 (hook vs skill — deterministic vs probabilistic) ➡️ Bài tiếp theo: Bài 2.12 — Quiz tổng hợp Claude Code 101Tài liệu tham khảo
- Claude Code Hooks documentation — Anthropic official docs
- Claude Code settings.json reference — cấu trúc đầy đủ
/hookscommand — gõ trong Claude Code session để xem và edit hooks interactivelyCLAUDE_PROJECT_DIRenv var — inject tự động bởi Claude Code khi chạy hook script