Tôi dùng Claude Code mấy tháng trước khi phát hiện hooks. Giờ không thể tưởng tượng làm việc thiếu chúng. Hooks là scripts tự động chạy trước hoặc sau khi Claude Code thực hiện một hành động — và chúng mở ra cả một danh mục workflows mà prompts đơn thuần không thể làm được.
Muốn tự động lint mỗi file Claude edit? Hook. Muốn chặn lệnh nguy hiểm mà không chỉ dựa vào deny list? Hook. Muốn log mọi hành động Claude làm để audit? Hook. Muốn auto-run tests sau mỗi thay đổi code? Hook.
Đây là mọi thứ tôi đã học về xây dựng hooks hiệu quả, kèm ví dụ thật bạn có thể áp dụng ngay.
Hooks Là Gì?
Hooks là scripts (bash, python, hay gì cũng được) mà Claude Code execute ở những thời điểm cụ thể trong workflow. Chúng được cấu hình trong .claude/settings.json và chạy tự động — không cần prompting.
Khi bài này viết lần đầu, Claude Code có 4 hook events. Giờ đã có 17. Hệ thống đã mở rộng đáng kể — từ những hook cơ bản ban đầu đến coverage gần như toàn bộ vòng đời của agent.
Dưới đây là toàn bộ 17 hook events, nhóm theo lifecycle:
Session Lifecycle
| Hook | Khi Nào Chạy | Blocking? | Use Case |
|---|---|---|---|
SessionStart | Đầu mỗi session | Không | Load config, khởi tạo môi trường |
PreCompact | Trước khi compact context | Có | Lưu trạng thái quan trọng trước khi nén |
SessionEnd | Cuối session | Không | Cleanup, tóm tắt, ghi log session |
User Interaction
| Hook | Khi Nào Chạy | Blocking? | Use Case |
|---|---|---|---|
UserPromptSubmit | Khi user gửi prompt | Có | Validate input, inject context tự động |
Notification | Khi Claude gửi notification | Không | Custom alerts, routing thông báo |
Tool Lifecycle
| Hook | Khi Nào Chạy | Blocking? | Use Case |
|---|---|---|---|
PreToolUse | Trước khi Claude chạy tool | Có | Chặn lệnh nguy hiểm, validate input |
PermissionRequest | Khi Claude xin quyền | Có | Auto-approve/deny theo policy |
PostToolUse | Sau khi Claude chạy tool thành công | Không | Lint file đã edit, log actions |
PostToolUseFailure | Sau khi tool thất bại | Không | Log lỗi, trigger recovery |
Agent Lifecycle
| Hook | Khi Nào Chạy | Blocking? | Use Case |
|---|---|---|---|
SubagentStart | Khi subagent được spawn | Không | Inject context cho subagent |
SubagentStop | Khi subagent kết thúc | Không | Collect results, log output |
Stop | Khi Claude dừng turn | Có | Tóm tắt thay đổi, chạy tests |
Team Events
| Hook | Khi Nào Chạy | Blocking? | Use Case |
|---|---|---|---|
TeammateIdle | Khi teammate agent idle | Không | Assign task mới, monitor progress |
TaskCompleted | Khi task được mark complete | Không | Trigger downstream workflows |
Infrastructure
| Hook | Khi Nào Chạy | Blocking? | Use Case |
|---|---|---|---|
ConfigChange | Khi settings thay đổi | Không | Validate config, notify team |
WorktreeCreate | Khi tạo git worktree | Không | Setup môi trường isolated |
WorktreeRemove | Khi xóa git worktree | Không | Cleanup resources |
Mỗi hook nhận JSON payload qua stdin với chi tiết về hành động. Script đọc nó, xử lý, và exit với status code cho Claude biết phải làm gì.
4 Loại Handler
Đây là điểm thay đổi lớn nhất so với phiên bản đầu. Claude Code giờ hỗ trợ 4 loại handler khác nhau — không phải chỉ bash scripts.
1. command — Shell Script Truyền Thống
Nhận JSON qua stdin, exit code quyết định hành vi. Loại này được dùng trong tất cả ví dụ bên dưới.
{ "type": "command", "command": ".claude/hooks/block-dangerous.sh"}2. prompt — Gọi Claude Haiku Để Quyết Định
Không cần viết code. Bạn viết prompt bằng tiếng Anh (hoặc tiếng Việt), Claude Haiku đọc JSON payload và trả lời yes/no.
{ "type": "prompt", "prompt": "Review this bash command for safety. If it contains rm -rf, force push, or drops a database table, respond with BLOCK and explain why. Otherwise respond with ALLOW."}Prompt hooks đặc biệt hữu ích cho team VN chưa quen viết bash scripts — bạn chỉ cần mô tả rule bằng ngôn ngữ tự nhiên. Không cần grep, không cần regex.
Ví dụ thực tế: kiểm tra commit message có theo convention không:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "prompt", "prompt": "Check if this git commit command follows conventional commits format (feat:, fix:, chore:, etc.). If the commit message doesn't follow the format, respond with BLOCK and show the correct format." } ] } ] }}3. agent — Subagent Nhiều Turn
Spawn một Claude subagent với quyền truy cập file đầy đủ (Read, Grep, Glob). Dùng cho những quyết định phức tạp cần đọc nhiều files.
{ "type": "agent", "prompt": "Analyze the edited file for security vulnerabilities. Read the file, check for SQL injection patterns, hardcoded credentials, and insecure dependencies. Report findings."}Agent hooks chạy như một mini-Claude session độc lập — nó có thể Grep codebase, đọc multiple files, và đưa ra quyết định có context đầy đủ.
4. http — POST Đến Webhook
Zero scripting. Gửi JSON payload đến bất kỳ URL nào — Slack, Discord, n8n, custom API.
{ "type": "http", "url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"}Kết hợp với PostToolUse để notify team mỗi khi Claude deploy hoặc merge code:
{ "hooks": { "PostToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "http", "url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL" } ] } ] }}Hook Đầu Tiên: Chặn Lệnh Nguy Hiểm
Bắt đầu với hook thực tế nhất — chặn lệnh nguy hiểm. Đây là hook từ bài bảo mật được nâng cấp thêm.
Bước 1: Tạo Hook Script
#!/bin/bashinput=$(cat)tool_name=$(echo "$input" | jq -r '.tool_name // ""')command=$(echo "$input" | jq -r '.tool_input.command // ""')
# Chỉ kiểm tra Bash commandsif [ "$tool_name" != "Bash" ]; then exit 0fi
# Chặn xóa fileif echo "$command" | grep -qE '\brm\s+'; then echo '{"error": "BLOCKED: rm không được phép. Xóa file thủ công."}' >&2 exit 2fi
# Chặn force pushif echo "$command" | grep -qE 'git\s+push\s+.*(-f|--force)'; then echo '{"error": "BLOCKED: Force push không được phép."}' >&2 exit 2fi
exit 0Bước 2: Cấp Quyền Thực Thi
chmod +x .claude/hooks/block-dangerous.shBước 3: Đăng Ký Trong Settings
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": ".claude/hooks/block-dangerous.sh" } ] } ] }}Giờ mọi Bash command Claude chạy đều qua script trước. Nếu match pattern nguy hiểm, script trả exit code 2 kèm error message, và Claude thấy lệnh bị chặn.
Hook 2: Auto-Lint Sau Khi Edit
Hook này tiết kiệm 5-10 prompts mỗi session. Mỗi khi Claude edit file, hook tự động chạy linter.
#!/bin/bashinput=$(cat)tool_name=$(echo "$input" | jq -r '.tool_name // ""')file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')
# Chỉ chạy sau Edit toolif [ "$tool_name" != "Edit" ]; then exit 0fi
# Lấy file extensionext="${file_path##*.}"
case "$ext" in ts|tsx) npx eslint --fix "$file_path" 2>/dev/null ;; py) python -m black "$file_path" 2>/dev/null ;; kt) ktlint --format "$file_path" 2>/dev/null ;;esac
exit 0Đăng ký là PostToolUse hook:
{ "hooks": { "PostToolUse": [ { "matcher": "Edit", "hooks": [ { "type": "command", "command": ".claude/hooks/auto-lint.sh" } ] } ] }}Giờ code Claude edit được auto-format. Không cần “chạy prettier đi” nữa.
Hook 3: Action Logger
Cho team cần audit việc dùng AI tool, hook này log mọi hành động Claude thực hiện.
#!/bin/bashinput=$(cat)tool_name=$(echo "$input" | jq -r '.tool_name // ""')timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Tạo log entrylog_entry=$(echo "$input" | jq -c --arg ts "$timestamp" '{ timestamp: $ts, tool: .tool_name, input: .tool_input}')
# Ghi vào file logecho "$log_entry" >> .claude/audit.jsonl
exit 0Đăng ký cho tất cả tools:
{ "hooks": { "PreToolUse": [ { "matcher": "", "hooks": [ { "type": "command", "command": ".claude/hooks/audit-log.sh" } ] } ] }}matcher rỗng match tất cả tools. Mọi hành động được log với timestamp, tool name, và input parameters. Vàng cho compliance và debugging.
Hook 4: Secret Detection
Hook này scan mọi file Claude đọc hoặc edit để phát hiện secrets bị lộ.
#!/bin/bashinput=$(cat)tool_name=$(echo "$input" | jq -r '.tool_name // ""')
# Kiểm tra file operationsfile_path=""if [ "$tool_name" = "Edit" ] || [ "$tool_name" = "Write" ]; then file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')fi
if [ -z "$file_path" ] || [ ! -f "$file_path" ]; then exit 0fi
# Scan patterns secret phổ biếnif grep -qE '(AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36})' "$file_path"; then echo '{"error": "BLOCKED: Phát hiện secret tiềm năng trong file. Review trước khi tiếp tục."}' >&2 exit 2fi
exit 0Hook này bắt AWS access keys, API keys với prefix phổ biến, và GitHub tokens trước khi chúng bị commit.
Hook 5: Test Runner Sau Thay Đổi
Hook workflow mạnh nhất — tự động chạy tests liên quan khi Claude sửa source files.
#!/bin/bashinput=$(cat)tool_name=$(echo "$input" | jq -r '.tool_name // ""')
# Chỉ trigger sau Editif [ "$tool_name" != "Edit" ]; then exit 0fi
file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')
# Map source file sang test filetest_file=""if [[ "$file_path" == *".ts" ]]; then test_file="${file_path%.ts}.test.ts"elif [[ "$file_path" == *".kt" ]]; then test_file=$(echo "$file_path" | sed 's|/main/|/test/|') test_file="${test_file%.kt}Test.kt"fi
# Chạy test nếu tồn tạiif [ -n "$test_file" ] && [ -f "$test_file" ]; then echo "Running tests for: $test_file" >&2 if [[ "$file_path" == *".ts" ]]; then npx jest "$test_file" --silent 2>&1 | tail -5 >&2 elif [[ "$file_path" == *".kt" ]]; then ./gradlew test --tests "$(basename ${test_file%.kt})" 2>&1 | tail -5 >&2 fifi
exit 0Lưu ý: Hook này chạy tests như side effect nhưng luôn exit 0, nên không chặn workflow của Claude. Nó chỉ hiện kết quả test trong output để Claude thấy nếu có gì break.
Nâng Cao: Kết Hợp Hooks
Sức mạnh thực sự là kết hợp nhiều hooks. Đây là phần hooks hoàn chỉnh trong .claude/settings.json của tôi:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": ".claude/hooks/block-dangerous.sh" } ] }, { "matcher": "", "hooks": [ { "type": "command", "command": ".claude/hooks/audit-log.sh" } ] }, { "matcher": "Write", "hooks": [ { "type": "command", "command": ".claude/hooks/secret-scan.sh" } ] } ], "PostToolUse": [ { "matcher": "Edit", "hooks": [ { "type": "command", "command": ".claude/hooks/auto-lint.sh" }, { "type": "command", "command": ".claude/hooks/auto-test.sh" } ] } ] }}Thứ tự thực thi:
- Trước mọi action: Log nó
- Trước Bash: Kiểm tra lệnh nguy hiểm
- Trước Write: Scan secrets
- Sau Edit: Auto-lint, rồi chạy tests
Tính Năng Nâng Cao
Matchers: Lọc Chính Xác Bằng Regex
Matcher không chỉ là tên tool — nó là regex. Điều này cho phép lọc rất linh hoạt:
{ "matcher": "Bash", "hooks": [...]}{ "matcher": "Edit|Write", "hooks": [...]}{ "matcher": "mcp__.*", "hooks": [...]}Cái cuối đặc biệt hữu ích: match tất cả MCP tools mà không cần list từng cái. Khi team dùng nhiều MCP servers khác nhau, một matcher regex tiết kiệm rất nhiều config lặp lại.
Async Hooks
Mặc định hooks chạy synchronous và Claude chờ kết quả. Nếu hook cần chạy nặng (build full, deploy), dùng async: true:
{ "type": "command", "command": ".claude/hooks/notify-deploy.sh", "async": true}Claude tiếp tục ngay mà không chờ hook kết thúc. Phù hợp cho notifications và side effects không cần blocking.
CLAUDE_ENV_FILE: Inject Env Vars Vào Hook
Cần pass secrets hoặc config vào hook mà không hardcode? Dùng CLAUDE_ENV_FILE:
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...JIRA_TOKEN=your-token-here{ "type": "command", "command": ".claude/hooks/notify-slack.sh", "env_file": ".claude/hooks/.env"}Script có thể đọc các biến này từ environment. File .env không bao giờ commit lên git — thêm vào .gitignore.
Chống Loop Vô Hạn Với stop_hook_active
Stop hooks có thể tự trigger lại chính chúng nếu không cẩn thận. Claude Code inject biến $CLAUDE_STOP_HOOK_ACTIVE để bạn check:
#!/bin/bash# Tránh loop vô hạnif [ "$CLAUDE_STOP_HOOK_ACTIVE" = "true" ]; then exit 0fi
# Logic thực sự của hookinput=$(cat)# ... xử lý ...
exit 0Luôn check biến này trong Stop hooks. Thiếu check này là lý do phổ biến nhất khiến session bị treo.
Hook Scoping: User vs Project vs Local
Hooks có thể được định nghĩa ở nhiều cấp độ khác nhau:
| Scope | File | Áp Dụng Cho |
|---|---|---|
user | ~/.claude/settings.json | Tất cả projects của bạn |
project | .claude/settings.json | Project cụ thể (commit vào repo) |
local | .claude/settings.local.json | Project cụ thể (không commit) |
Thực tế: Security hooks (block-dangerous, secret-scan) nên ở user scope — chúng áp dụng mọi nơi. Project-specific hooks (auto-lint với config riêng, test runner) ở project scope. Personal preferences ở local scope.
Nguyên Tắc Thiết Kế Hook
Sau khi xây hàng chục hooks, đây là nguyên tắc tôi theo:
1. Hooks phải nhanh.
PreToolUse hooks chạy synchronous — hooks chậm làm Claude cảm giác lag. Giữ dưới 500ms. Nếu cần chạy gì chậm (như full test suite), làm trong PostToolUse và dùng async: true hoặc đừng block kết quả.
2. Dùng exit codes đúng.
exit 0= thành công, tiếp tục bình thườngexit 2= chặn action (kèm error message trên stderr)- Non-zero khác = error, nhưng không chặn
3. Luôn xử lý missing data.
JSON payload có thể thiếu field. Dùng jq -r '.field // ""' với defaults, và luôn kiểm tra trước khi dùng giá trị.
4. Đừng sửa output của Claude. Hooks có thể chặn actions hoặc thêm side effects, nhưng không nên thay đổi những gì Claude produce. Điều đó tạo tình huống debug rối loạn.
5. Log nhiều. Hooks chạy im lặng. Khi có lỗi, logs là cửa sổ duy nhất để biết chuyện gì xảy ra.
Debug Hooks
Khi hook không hoạt động:
-
Test standalone — pipe JSON mẫu vào script thủ công:
Terminal window echo '{"tool_name":"Bash","tool_input":{"command":"rm file.txt"}}' | .claude/hooks/block-dangerous.shecho $? # Phải là 2 -
Kiểm tra permissions — script phải executable:
Terminal window ls -la .claude/hooks/ -
Kiểm tra jq — hầu hết hooks phụ thuộc vào nó:
Terminal window which jq -
Kiểm tra matcher — matcher rỗng match mọi thứ, matcher cụ thể như
"Bash"chỉ match tool đó.
Tóm Lại
- Hooks là extension system của Claude Code. Tùy chỉnh behavior mà không đổi prompts.
- 17 hook events cover toàn bộ vòng đời — từ session start đến worktree cleanup.
- 4 loại handler cho mọi use case:
commandcho logic phức tạp,promptcho team không quen bash,agentcho phân tích đa file,httpcho webhook integration. - PreToolUse hooks là tầng bảo mật. Chặn lệnh nguy hiểm, scan secrets, validate inputs.
- PostToolUse hooks tự động hóa thao tác nhàm chán. Auto-lint, auto-test, auto-format.
- Giữ hooks nhanh và đơn giản. Dưới 500ms, exit codes rõ ràng, xử lý missing data.
- Kết hợp hooks cho defense in depth. Nhiều hooks nhẹ thắng một monolith phức tạp.
Hooks biến Claude Code từ coding assistant thông minh thành development platform tùy chỉnh được. Khi bắt đầu xây chúng, bạn sẽ tự hỏi sao trước đây làm việc thiếu chúng.
Muốn đi sâu hơn? Xem Claude Code Hooks Nâng Cao: Patterns Thực Tế Cho Production — prompt hooks, agent hooks, và các pattern phức tạp hơn.
Hệ thống hooks được trình bày chi tiết trong Phase 11: Automation & Headless của khóa học Claude Code Mastery. Phases 1-3 miễn phí.