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

HookKhi Nào ChạyBlocking?Use Case
SessionStartĐầu mỗi sessionKhôngLoad config, khởi tạo môi trường
PreCompactTrước khi compact contextLưu trạng thái quan trọng trước khi nén
SessionEndCuối sessionKhôngCleanup, tóm tắt, ghi log session

User Interaction

HookKhi Nào ChạyBlocking?Use Case
UserPromptSubmitKhi user gửi promptValidate input, inject context tự động
NotificationKhi Claude gửi notificationKhôngCustom alerts, routing thông báo

Tool Lifecycle

HookKhi Nào ChạyBlocking?Use Case
PreToolUseTrước khi Claude chạy toolChặn lệnh nguy hiểm, validate input
PermissionRequestKhi Claude xin quyềnAuto-approve/deny theo policy
PostToolUseSau khi Claude chạy tool thành côngKhôngLint file đã edit, log actions
PostToolUseFailureSau khi tool thất bạiKhôngLog lỗi, trigger recovery

Agent Lifecycle

HookKhi Nào ChạyBlocking?Use Case
SubagentStartKhi subagent được spawnKhôngInject context cho subagent
SubagentStopKhi subagent kết thúcKhôngCollect results, log output
StopKhi Claude dừng turnTóm tắt thay đổi, chạy tests

Team Events

HookKhi Nào ChạyBlocking?Use Case
TeammateIdleKhi teammate agent idleKhôngAssign task mới, monitor progress
TaskCompletedKhi task được mark completeKhôngTrigger downstream workflows

Infrastructure

HookKhi Nào ChạyBlocking?Use Case
ConfigChangeKhi settings thay đổiKhôngValidate config, notify team
WorktreeCreateKhi tạo git worktreeKhôngSetup môi trường isolated
WorktreeRemoveKhi xóa git worktreeKhôngCleanup 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

.claude/hooks/block-dangerous.sh
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // ""')
command=$(echo "$input" | jq -r '.tool_input.command // ""')
# Chỉ kiểm tra Bash commands
if [ "$tool_name" != "Bash" ]; then
exit 0
fi
# Chặn xóa file
if echo "$command" | grep -qE '\brm\s+'; then
echo '{"error": "BLOCKED: rm không được phép. Xóa file thủ công."}' >&2
exit 2
fi
# Chặn force push
if echo "$command" | grep -qE 'git\s+push\s+.*(-f|--force)'; then
echo '{"error": "BLOCKED: Force push không được phép."}' >&2
exit 2
fi
exit 0

Bước 2: Cấp Quyền Thực Thi

Terminal window
chmod +x .claude/hooks/block-dangerous.sh

Bướ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.

.claude/hooks/auto-lint.sh
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // ""')
file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')
# Chỉ chạy sau Edit tool
if [ "$tool_name" != "Edit" ]; then
exit 0
fi
# Lấy file extension
ext="${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.

.claude/hooks/audit-log.sh
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // ""')
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Tạo log entry
log_entry=$(echo "$input" | jq -c --arg ts "$timestamp" '{
timestamp: $ts,
tool: .tool_name,
input: .tool_input
}')
# Ghi vào file log
echo "$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ộ.

.claude/hooks/secret-scan.sh
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // ""')
# Kiểm tra file operations
file_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 0
fi
# Scan patterns secret phổ biến
if 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 2
fi
exit 0

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

.claude/hooks/auto-test.sh
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // ""')
# Chỉ trigger sau Edit
if [ "$tool_name" != "Edit" ]; then
exit 0
fi
file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')
# Map source file sang test file
test_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ại
if [ -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
fi
fi
exit 0

Lư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:

  1. Trước mọi action: Log nó
  2. Trước Bash: Kiểm tra lệnh nguy hiểm
  3. Trước Write: Scan secrets
  4. 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:

.claude/hooks/.env
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:

.claude/hooks/on-stop.sh
#!/bin/bash
# Tránh loop vô hạn
if [ "$CLAUDE_STOP_HOOK_ACTIVE" = "true" ]; then
exit 0
fi
# Logic thực sự của hook
input=$(cat)
# ... xử lý ...
exit 0

Luô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:

ScopeFileÁp Dụng Cho
user~/.claude/settings.jsonTất cả projects của bạn
project.claude/settings.jsonProject cụ thể (commit vào repo)
local.claude/settings.local.jsonProject 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ường
  • exit 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:

  1. 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.sh
    echo $? # Phải là 2
  2. Kiểm tra permissions — script phải executable:

    Terminal window
    ls -la .claude/hooks/
  3. Kiểm tra jq — hầu hết hooks phụ thuộc vào nó:

    Terminal window
    which jq
  4. Kiểm tra matcher — matcher rỗng match mọi thứ, matcher cụ thể như "Bash" chỉ match tool đó.


Tóm Lại

  1. Hooks là extension system của Claude Code. Tùy chỉnh behavior mà không đổi prompts.
  2. 17 hook events cover toàn bộ vòng đời — từ session start đến worktree cleanup.
  3. 4 loại handler cho mọi use case: command cho logic phức tạp, prompt cho team không quen bash, agent cho phân tích đa file, http cho webhook integration.
  4. PreToolUse hooks là tầng bảo mật. Chặn lệnh nguy hiểm, scan secrets, validate inputs.
  5. PostToolUse hooks tự động hóa thao tác nhàm chán. Auto-lint, auto-test, auto-format.
  6. Giữ hooks nhanh và đơn giản. Dưới 500ms, exit codes rõ ràng, xử lý missing data.
  7. 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í.