Bài hooks cơ bản tôi viết vài tháng trước dạy bốn events và shell scripts. Đủ để bắt đầu.
Nhưng kể từ đó, hooks đã phát triển xa hơn nhiều. Claude Code giờ có 17 hook events. Và quan trọng hơn — ngoài shell scripts, còn có ba loại handler khác mà hầu hết developer chưa biết đến: prompt hooks gọi Claude Haiku để đưa ra quyết định, agent hooks spawn subagent với quyền đọc file, và HTTP hooks gọi webhook bên ngoài mà không cần viết một dòng bash.
Hệ thống hooks cơ bản dạy bạn cách chặn lệnh nguy hiểm và tự động format code. Bài này là phần tiếp theo — những gì bạn chưa biết hooks có thể làm.
17 Hook Events: Toàn Cảnh
Nhóm theo vòng đời để dễ nhìn:
Session Lifecycle
| Event | Khi Nào Chạy | Có Thể Chặn? | Matchers |
|---|---|---|---|
SessionStart | Session bắt đầu hoặc resume | Không | startup, resume, clear, compact |
PreCompact | Trước khi context bị nén | Không | manual, auto |
SessionEnd | Session kết thúc | Không | clear, logout, prompt_input_exit |
User Interaction
| Event | Khi Nào Chạy | Có Thể Chặn? |
|---|---|---|
UserPromptSubmit | Trước khi Claude xử lý prompt của bạn | Có (xóa prompt) |
Notification | Khi Claude gửi notification | Không |
Tool Lifecycle
| Event | Khi Nào Chạy | Có Thể Chặn? | Matchers |
|---|---|---|---|
PreToolUse | Trước khi tool thực thi | Có | regex tên tool |
PermissionRequest | Khi dialog xin quyền xuất hiện | Có | regex tên tool |
PostToolUse | Sau khi tool thành công | Không (gửi feedback) | regex tên tool |
PostToolUseFailure | Sau khi tool thất bại | Không (gửi feedback) | regex tên tool |
Agent Lifecycle
| Event | Khi Nào Chạy | Có Thể Chặn? |
|---|---|---|
SubagentStart | Khi subagent được spawn | Không |
SubagentStop | Khi subagent hoàn thành | Có |
Stop | Khi Claude kết thúc turn | Có (buộc tiếp tục) |
Team Events
| Event | Khi Nào Chạy | Có Thể Chặn? |
|---|---|---|
TeammateIdle | Teammate sắp chuyển sang idle | Có |
TaskCompleted | Task sắp được đánh dấu hoàn thành | Có |
Infrastructure
| Event | Khi Nào Chạy | Có Thể Chặn? |
|---|---|---|
ConfigChange | Settings thay đổi giữa session | Có (trừ policy) |
WorktreeCreate | Worktree đang được tạo | Có (thay thế default) |
WorktreeRemove | Worktree đang bị xóa | Không |
13 events trong bảng trên là những gì hầu hết developer bỏ qua. Đặc biệt TaskCompleted và TeammateIdle rất mạnh khi làm việc theo nhóm.
Vượt Ra Ngoài Shell Scripts: 4 Loại Handler
Đây là phần nhiều người không biết. Hooks không chỉ có command type.
1. Command (Bạn Đã Biết)
Shell script nhận JSON từ stdin, trả về exit code và output.
{ "type": "command", "command": ".claude/hooks/my-script.sh", "timeout": 30}Tốt cho: file operations, external tools, custom logic phức tạp.
2. Prompt (AI Ra Quyết Định — Không Cần Code)
Gọi Claude Haiku để đưa ra quyết định yes/no. Không cần viết bash script, không cần xử lý JSON thủ công.
{ "type": "prompt", "prompt": "Kiểm tra xem tất cả yêu cầu của user đã được thực hiện chưa. Context: $ARGUMENTS. Trả về: {\"ok\": true} hoặc {\"ok\": false, \"reason\": \"...\"}", "model": "claude-haiku-4-5", "timeout": 30}$ARGUMENTS được thay thế bởi JSON payload của hook event. Claude Haiku đọc context và trả về quyết định. Không cần một dòng code nào.
Tốt cho: content validation, completion checks, intelligent gate logic mà không muốn code.
3. Agent (Subagent Với Tool Access)
Spawn một subagent có đầy đủ khả năng đọc file, search codebase, chạy lệnh. Đây là hooks mạnh nhất.
{ "type": "agent", "prompt": "Xác minh tất cả unit tests đã pass. Chạy test suite và kiểm tra kết quả. $ARGUMENTS", "timeout": 120}Agent này có thể dùng Read, Grep, Glob, Bash — tức là nó thực sự chạy tests và đọc kết quả, không chỉ kiểm tra file tồn tại.
Tốt cho: complex verification, thực sự kiểm tra chất lượng code trước khi cho phép tiếp tục.
4. HTTP (Webhook — Zero Scripting)
POST JSON đến một URL. Response quyết định hook có block hay không.
{ "type": "http", "url": "http://localhost:8080/hooks/tool-use", "headers": { "Authorization": "Bearer $MY_TOKEN" }, "allowedEnvVars": ["MY_TOKEN"], "timeout": 30}Tốt cho: tích hợp với monitoring system, audit logging, team dashboards, external services.
7 Công Thức Copy-Paste
Đây là phần chính. Mỗi công thức giải quyết một vấn đề thực tế, sẵn sàng dùng ngay.
Công Thức 1: AI Stop Gate
Vấn đề: Claude xong việc nhưng thực ra chưa hoàn thành hết yêu cầu. Bạn phải check lại thủ công.
Giải pháp: Dùng prompt hook để Claude Haiku verify trước khi cho phép dừng.
{ "hooks": { "Stop": [ { "hooks": [ { "type": "prompt", "prompt": "Review conversation history và kiểm tra: (1) Tất cả yêu cầu của user đã được thực hiện chưa? (2) Có TODO hay task nào còn bỏ dở không? (3) Code có compile và pass tests không nếu applicable? Context: $ARGUMENTS. Nếu mọi thứ ổn, trả về {\"ok\": true}. Nếu còn việc chưa xong, trả về {\"ok\": false, \"reason\": \"Mô tả ngắn việc còn thiếu\"}", "model": "claude-haiku-4-5", "timeout": 30 } ] } ] }}Cách hoạt động: Mỗi khi Claude chuẩn bị dừng, Haiku review lại toàn bộ conversation. Nếu phát hiện còn việc chưa xong, nó block việc dừng và Claude phải tiếp tục.
Lưu ý quan trọng: Cần tránh infinite loop với Stop hooks. Thêm kiểm tra này nếu dùng command type thay vì prompt type:
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then exit 0 # Đã đang tiếp tục rồi — cho phép dừngfiPro tip: Hook này đặc biệt hữu ích cho developer chưa quen viết bash scripts. Với prompt type, không cần code gì cả — chỉ cần mô tả điều kiện bằng tiếng Anh (hoặc tiếng Việt) là được.
Công Thức 2: Agent Test Verification
Vấn đề: Claude claim “đã xong” nhưng tests thực ra đang fail. Bạn phát hiện khi đã push code.
Giải pháp: Agent hook spawn một subagent thực sự chạy test suite trước khi cho phép task được đánh dấu hoàn thành.
{ "hooks": { "TaskCompleted": [ { "hooks": [ { "type": "agent", "prompt": "Trước khi task này được đánh dấu hoàn thành, hãy: (1) Chạy test suite của project (npm test, pytest, hoặc tương đương). (2) Kiểm tra xem có test nào fail không. (3) Nếu có test fail, trả về {\"ok\": false, \"reason\": \"Tests failing: [danh sách tests]\"}. (4) Nếu tất cả tests pass, trả về {\"ok\": true}. Context: $ARGUMENTS", "timeout": 120 } ] } ] }}Cách hoạt động: Agent có tool access — nó thực sự chạy tests, đọc output, và đưa ra quyết định dựa trên kết quả thật, không phải giả định.
Pro tip: Set timeout cao hơn nếu test suite của bạn chậm. 120 giây thường đủ cho hầu hết projects.
Công Thức 3: Session Context Survival
Vấn đề: Sau khi /compact (hoặc Claude tự compact khi context đầy), tất cả context quan trọng về sprint hiện tại, tech stack rules, và active branch biến mất. Claude bắt đầu mắc những lỗi cơ bản.
Giải pháp: SessionStart hook với matcher compact re-inject context quan trọng sau mỗi lần compact.
Tạo file context:
#!/bin/bash
input=$(cat)trigger=$(echo "$input" | jq -r '.trigger // ""')
# Chỉ inject khi resume hoặc sau compactif [ "$trigger" != "compact" ] && [ "$trigger" != "resume" ]; then exit 0fi
PROJECT_CONTEXT=$(cat .claude/project-context.md 2>/dev/null || echo "")CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")ACTIVE_SPRINT=$(cat .claude/current-sprint.md 2>/dev/null || echo "")
# Inject qua CLAUDE_ENV_FILEif [ -n "$CLAUDE_ENV_FILE" ]; then echo "export PROJECT_BRANCH=$CURRENT_BRANCH" >> "$CLAUDE_ENV_FILE"fi
# Ghi context ra stdout để Claude đọccat << EOF=== CONTEXT RESTORED AFTER COMPACTION ===Branch hiện tại: $CURRENT_BRANCH$ACTIVE_SPRINT$PROJECT_CONTEXT=== END CONTEXT ===EOF
exit 0{ "hooks": { "SessionStart": [ { "matcher": "compact", "hooks": [ { "type": "command", "command": ".claude/hooks/inject-context.sh" } ] }, { "matcher": "resume", "hooks": [ { "type": "command", "command": ".claude/hooks/inject-context.sh" } ] } ] }}Cách hoạt động: CLAUDE_ENV_FILE là một tính năng đặc biệt của SessionStart — nếu variable này tồn tại, bạn có thể append environment variables vào đó và chúng sẽ tồn tại suốt session. Kết hợp với stdout output, Claude nhận đủ context để tiếp tục chính xác.
Pro tip: Lưu sprint info, tech stack decisions, và “đừng làm X” rules vào .claude/project-context.md. Hook này sẽ tự inject chúng sau mỗi lần compact.
Công Thức 4: Async Test Runner
Vấn đề: Chạy tests sau mỗi file edit làm Claude phải chờ, cảm giác lag. Nhưng nếu không chạy thì không biết khi nào tests break.
Giải pháp: PostToolUse async hook — tests chạy background trong khi Claude tiếp tục làm việc. Kết quả xuất hiện ở turn tiếp theo.
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": ".claude/hooks/async-test-runner.sh", "async": true, "timeout": 300 } ] } ] }}#!/bin/bashinput=$(cat)file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')
# Chỉ chạy với source files (không phải test files)if [[ "$file_path" == *".test."* ]] || [[ "$file_path" == *".spec."* ]]; then exit 0fi
ext="${file_path##*.}"RESULT_FILE=".claude/test-results-$(date +%s).txt"
case "$ext" in ts|tsx|js|jsx) npx jest --passWithNoTests 2>&1 | tail -20 > "$RESULT_FILE" ;; py) python -m pytest --tb=short 2>&1 | tail -20 > "$RESULT_FILE" ;; kt) ./gradlew test 2>&1 | tail -20 > "$RESULT_FILE" ;;esac
# Đọc result và in ra — sẽ xuất hiện ở turn tiếp theoif [ -f "$RESULT_FILE" ]; then echo "=== Test Results ===" cat "$RESULT_FILE" rm "$RESULT_FILE"fi
exit 0Cách hoạt động: "async": true là tất cả những gì cần. Claude không chờ hook này — nó tiếp tục làm việc. Khi tests xong, kết quả được ghi vào context cho turn tiếp theo.
Pro tip: Thêm filter để không chạy tests khi sửa test files — tránh vòng lặp vô tận.
Công Thức 5: MCP Tool Auditing
Vấn đề: Claude đang dùng MCP tools để gọi external services (database, APIs, memory) nhưng bạn không biết chính xác những gì đang được gọi. Không có visibility.
Giải pháp: PostToolUse hook với regex matcher để catch tất cả MCP tool calls.
{ "hooks": { "PostToolUse": [ { "matcher": "mcp__.*", "hooks": [ { "type": "command", "command": ".claude/hooks/mcp-audit.sh" } ] } ] }}#!/bin/bashinput=$(cat)tool_name=$(echo "$input" | jq -r '.tool_name // ""')timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")session_id=$(echo "$input" | jq -r '.session_id // "unknown"')
# Parse MCP server và tool từ tên (format: mcp__server__tool)mcp_server=$(echo "$tool_name" | cut -d'_' -f3)mcp_tool=$(echo "$tool_name" | cut -d'_' -f4-)
# Ghi audit loglog_entry=$(echo "$input" | jq -c \ --arg ts "$timestamp" \ --arg server "$mcp_server" \ --arg tool "$mcp_tool" \ '{ timestamp: $ts, mcp_server: $server, mcp_tool: $tool, full_tool: .tool_name, input_summary: (.tool_input | to_entries | map(.key) | join(", ")), success: (.tool_response.is_error | not) }')
echo "$log_entry" >> .claude/mcp-audit.jsonl
exit 0Cách hoạt động: Pattern mcp__.* match tất cả MCP tools. Nếu chỉ muốn audit một server cụ thể, dùng mcp__memory__.* hoặc mcp__database__.*. Pattern cũng có thể match theo action: mcp__.*__write.* bắt tất cả write operations trên mọi MCP server.
Pro tip: Sinh viên FPT làm intern tại các công ty có compliance requirements — hook này tạo audit trail cần thiết mà không ảnh hưởng performance.
Công Thức 6: Team Quality Gate
Vấn đề: Trong team Agile, có teammate mark task “done” dù code chưa pass tests hoặc chưa có review. Sprint velocity trông đẹp trên bảng nhưng thực tế backlog đang tích lũy hidden debt.
Giải pháp: TaskCompleted hook kiểm tra chất lượng trước khi cho phép mark done.
{ "hooks": { "TaskCompleted": [ { "hooks": [ { "type": "command", "command": ".claude/hooks/quality-gate.sh", "timeout": 60 } ] } ] }}#!/bin/bashinput=$(cat)task_description=$(echo "$input" | jq -r '.task_description // ""')ERRORS=()
# Kiểm tra 1: Tests phải passif [ -f "package.json" ] && grep -q '"test"' package.json; then TEST_OUTPUT=$(npx jest --passWithNoTests --silent 2>&1) TEST_EXIT=$? if [ $TEST_EXIT -ne 0 ]; then ERRORS+=("Tests đang fail. Chạy npm test để xem chi tiết.") fifi
# Kiểm tra 2: Không có TypeScript errorsif [ -f "tsconfig.json" ]; then TS_OUTPUT=$(npx tsc --noEmit 2>&1) if [ $? -ne 0 ]; then TS_ERRORS=$(echo "$TS_OUTPUT" | grep "error TS" | wc -l | tr -d ' ') ERRORS+=("Có $TS_ERRORS TypeScript error(s). Chạy tsc --noEmit để xem.") fifi
# Kiểm tra 3: Không có uncommitted changes lớnCHANGED_FILES=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ')if [ "$CHANGED_FILES" -gt 10 ]; then ERRORS+=("Có $CHANGED_FILES files chưa commit. Review và commit trước khi mark done.")fi
# Kết quảif [ ${#ERRORS[@]} -gt 0 ]; then ERROR_LIST=$(printf '%s\n' "${ERRORS[@]}" | jq -R . | jq -s 'join("; ")') echo "{\"ok\": false, \"reason\": \"Quality gate failed: $ERROR_LIST\"}" exit 2fi
exit 0Cách hoạt động: Hook chạy ba kiểm tra tuần tự trước khi cho phép TaskCompleted. Nếu bất kỳ kiểm tra nào fail, task không thể được đánh dấu hoàn thành và Claude nhận được lý do cụ thể để fix.
Pro tip: Customize các kiểm tra theo tiêu chuẩn của team. Các team Agile ở Việt Nam làm việc với nhiều stakeholder — hook này đặc biệt hữu ích cho sprint review khi cần đảm bảo “done” thực sự là “done”.
Công Thức 7: Webhook Integration
Vấn đề: Bạn muốn tracking mọi thứ Claude làm — build dashboards, gửi alerts khi có file quan trọng bị sửa, tích hợp với monitoring system. Nhưng không muốn viết và maintain bash scripts phức tạp.
Giải pháp: HTTP hook — zero scripting, chỉ cần một URL.
{ "hooks": { "PreToolUse": [ { "matcher": "Edit|Write|Bash", "hooks": [ { "type": "http", "url": "https://your-monitoring-service.com/api/claude-hooks", "headers": { "Authorization": "Bearer $MONITORING_API_KEY", "Content-Type": "application/json", "X-Project": "my-project" }, "allowedEnvVars": ["MONITORING_API_KEY"], "timeout": 10 } ] } ] }}Claude Code POST JSON payload của hook event đến URL đó. Response có thể block action (trả về {"ok": false, "reason": "..."}) hoặc cho phép tiếp tục (trả về {"ok": true} hoặc bất kỳ 2xx response).
Ví dụ minimal server nhận hooks (Node.js):
const express = require('express');const app = express();app.use(express.json());
app.post('/api/claude-hooks', (req, res) => { const { tool_name, tool_input, session_id } = req.body;
// Log vào database, gửi Slack notification, update dashboard... console.log(`[${new Date().toISOString()}] ${tool_name}`, { session: session_id, file: tool_input?.file_path || tool_input?.command });
// Cho phép tiếp tục (block bằng cách trả về 400 + reason) res.json({ ok: true });});
app.listen(3000, () => console.log('Hook server running on :3000'));Cách hoạt động: allowedEnvVars cho phép dùng environment variables trong headers mà không hard-code credentials. Hook tự động handle serialization, timeout, và retry.
Pro tip: Dùng hook này để tích hợp với các monitoring tools phổ biến tại VN như Grafana, DataDog, hay thậm chí n8n workflows. Một server nhỏ có thể aggregate hooks từ toàn bộ team và tạo team-level insights.
Matchers và Patterns Nâng Cao
Regex Matcher
Matcher là regex full, không chỉ exact match:
"Bash" → chỉ Bash tool"Edit|Write" → Edit hoặc Write"mcp__memory__.*" → tất cả tools từ memory MCP server"mcp__.*__write.*" → bất kỳ MCP tool nào có "write" trong tên".*" → tất cả tools (tương đương matcher rỗng)SessionStart Matchers
SessionStart có matchers đặc biệt khác với tool matchers:
| Matcher | Fires When |
|---|---|
startup | Session mới hoàn toàn |
resume | Resume session cũ |
clear | Sau lệnh /clear |
compact | Sau context compaction |
Async Hooks
Chỉ command type hỗ trợ async. Claude không chờ hook này hoàn thành:
{ "type": "command", "command": "./run-heavy-analysis.sh", "async": true, "timeout": 300}Dùng cho: test suites, build processes, bất kỳ thứ gì tốn hơn vài giây.
CLAUDE_ENV_FILE
Chỉ available trong SessionStart hooks. Cho phép inject environment variables cho toàn bộ session:
# Trong SessionStart hook scriptif [ -n "$CLAUDE_ENV_FILE" ]; then echo "export DATABASE_URL=postgres://localhost/mydb" >> "$CLAUDE_ENV_FILE" echo "export NODE_ENV=development" >> "$CLAUDE_ENV_FILE"fiCác variables này tồn tại suốt session và available trong mọi command Claude chạy.
Exit Codes
0 → thành công, xử lý JSON output2 → blocking error, chặn action (kèm JSON trên stderr)khác → non-blocking error, tiếp tục bình thườngDebug Hooks
Khi hook không hoạt động như mong đợi:
1. Dùng claude --debug để xem verbose output về hook execution.
2. Test standalone — pipe JSON mẫu vào script:
echo '{ "tool_name": "Bash", "tool_input": {"command": "rm -rf /"}, "session_id": "test-session"}' | .claude/hooks/block-dangerous.sh
echo "Exit code: $?"3. Kiểm tra JSON syntax — hook output phải là valid JSON:
echo '{"ok": false, "reason": "test"}' | jq .# Nếu jq parse được thì JSON hợp lệ4. Test HTTP hooks với curl trước:
curl -X POST https://your-service.com/hooks \ -H "Content-Type: application/json" \ -d '{"tool_name":"test","tool_input":{}}' \ -v5. Lỗi phổ biến:
- Script không có execute permission:
chmod +x script.sh jqchưa install:brew install jq(macOS) hoặcapt install jq- JSON output trên stdout thay vì stderr (cho blocking hooks, error message phải trên stderr)
- Infinite loop trong Stop hooks — luôn check
stop_hook_active
Hooks Là Lợi Thế Cạnh Tranh
Cursor không có hooks. GitHub Copilot không có hooks. Windsurf không có hooks.
Đây là lý do Claude Code đang trở thành một platform, không chỉ là một tool. Khi bạn có hooks, bạn có thể enforce team standards mà không cần rules trong CLAUDE.md. Bạn có thể tích hợp với monitoring systems. Bạn có thể đảm bảo quality gates được thực thi, không chỉ được đề xuất.
17 events và 4 handler types không phải là tính năng thừa — chúng là primitive cho phép bạn build bất kỳ automation nào cần thiết cho workflow cụ thể của mình.
Bắt đầu với một công thức từ bài này. Session Context Survival (công thức 3) hoặc Async Test Runner (công thức 4) thường là những cái đem lại giá trị ngay lập tức nhất. Khi đã quen, bạn sẽ thấy mình tự nhiên nghĩ “cái này hook được không?” cho mọi friction point trong workflow.
Tài nguyên tham khảo:
- Bài hooks cơ bản — nếu chưa đọc, đọc cái đó trước
- claude-code-hooks-mastery (GitHub) — 13 working hook examples
- claude-code-hooks-multi-agent-observability (GitHub) — multi-agent monitoring patterns
- GitButler Blog — auto-commit patterns với hooks
- Official Hooks Reference
Hooks được đào sâu trong Phase 11: Automation & Headless của khóa học Claude Code Mastery — multi-agent hooks, custom hook frameworks, và production deployment patterns. Phases 1-3 miễn phí.