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

EventKhi Nào ChạyCó Thể Chặn?Matchers
SessionStartSession bắt đầu hoặc resumeKhôngstartup, resume, clear, compact
PreCompactTrước khi context bị nénKhôngmanual, auto
SessionEndSession kết thúcKhôngclear, logout, prompt_input_exit

User Interaction

EventKhi Nào ChạyCó Thể Chặn?
UserPromptSubmitTrước khi Claude xử lý prompt của bạnCó (xóa prompt)
NotificationKhi Claude gửi notificationKhông

Tool Lifecycle

EventKhi Nào ChạyCó Thể Chặn?Matchers
PreToolUseTrước khi tool thực thiregex tên tool
PermissionRequestKhi dialog xin quyền xuất hiệnregex tên tool
PostToolUseSau khi tool thành côngKhông (gửi feedback)regex tên tool
PostToolUseFailureSau khi tool thất bạiKhông (gửi feedback)regex tên tool

Agent Lifecycle

EventKhi Nào ChạyCó Thể Chặn?
SubagentStartKhi subagent được spawnKhông
SubagentStopKhi subagent hoàn thành
StopKhi Claude kết thúc turnCó (buộc tiếp tục)

Team Events

EventKhi Nào ChạyCó Thể Chặn?
TeammateIdleTeammate sắp chuyển sang idle
TaskCompletedTask sắp được đánh dấu hoàn thành

Infrastructure

EventKhi Nào ChạyCó Thể Chặn?
ConfigChangeSettings thay đổi giữa sessionCó (trừ policy)
WorktreeCreateWorktree đang được tạoCó (thay thế default)
WorktreeRemoveWorktree đang bị xóaKhông

13 events trong bảng trên là những gì hầu hết developer bỏ qua. Đặc biệt TaskCompletedTeammateIdle 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:

Terminal window
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # Đã đang tiếp tục rồi — cho phép dừng
fi

Pro 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:

.claude/hooks/inject-context.sh
#!/bin/bash
input=$(cat)
trigger=$(echo "$input" | jq -r '.trigger // ""')
# Chỉ inject khi resume hoặc sau compact
if [ "$trigger" != "compact" ] && [ "$trigger" != "resume" ]; then
exit 0
fi
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_FILE
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo "export PROJECT_BRANCH=$CURRENT_BRANCH" >> "$CLAUDE_ENV_FILE"
fi
# Ghi context ra stdout để Claude đọc
cat << 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
}
]
}
]
}
}
.claude/hooks/async-test-runner.sh
#!/bin/bash
input=$(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 0
fi
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 theo
if [ -f "$RESULT_FILE" ]; then
echo "=== Test Results ==="
cat "$RESULT_FILE"
rm "$RESULT_FILE"
fi
exit 0

Cá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"
}
]
}
]
}
}
.claude/hooks/mcp-audit.sh
#!/bin/bash
input=$(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 log
log_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 0

Cá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
}
]
}
]
}
}
.claude/hooks/quality-gate.sh
#!/bin/bash
input=$(cat)
task_description=$(echo "$input" | jq -r '.task_description // ""')
ERRORS=()
# Kiểm tra 1: Tests phải pass
if [ -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.")
fi
fi
# Kiểm tra 2: Không có TypeScript errors
if [ -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+=("$TS_ERRORS TypeScript error(s). Chạy tsc --noEmit để xem.")
fi
fi
# Kiểm tra 3: Không có uncommitted changes lớn
CHANGED_FILES=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ')
if [ "$CHANGED_FILES" -gt 10 ]; then
ERRORS+=("$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 2
fi
exit 0

Cá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):

hooks-server.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:

MatcherFires When
startupSession mới hoàn toàn
resumeResume session cũ
clearSau lệnh /clear
compactSau 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:

Terminal window
# Trong SessionStart hook script
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo "export DATABASE_URL=postgres://localhost/mydb" >> "$CLAUDE_ENV_FILE"
echo "export NODE_ENV=development" >> "$CLAUDE_ENV_FILE"
fi

Cá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 output
2 → blocking error, chặn action (kèm JSON trên stderr)
khác → non-blocking error, tiếp tục bình thường

Debug 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:

Terminal window
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:

Terminal window
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:

Terminal window
curl -X POST https://your-service.com/hooks \
-H "Content-Type: application/json" \
-d '{"tool_name":"test","tool_input":{}}' \
-v

5. Lỗi phổ biến:

  • Script không có execute permission: chmod +x script.sh
  • jq chưa install: brew install jq (macOS) hoặc apt 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:


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