Hooks — Lớp kiểm soát: guardrail tất định cho agent
Suốt hai bài trước, ta liên tục gặp một câu cảnh báo: CLAUDE.md và Skills là chỉ dẫn — Claude cố tuân theo nhưng không đảm bảo tuyệt đối. Vậy khi bạn cần một điều chắc chắn xảy ra — luôn format code sau khi sửa, luôn quét secret trước khi commit, tuyệt đối chặn lệnh xóa dữ liệu — thì dựa vào đâu?
Câu trả lời là Hooks. Đây là lớp biến "mong muốn" thành "bảo đảm".
Hook là gì: điểm khác biệt cốt lõi
Hook là một lệnh hoặc script do bạn định nghĩa, chạy tất định (deterministic) tại những điểm cố định trong vòng đời tool và hội thoại của Claude Code. Điểm mấu chốt: mô hình KHÔNG quyết định hook có chạy hay không — khi sự kiện xảy ra và điều kiện khớp, hook luôn chạy (Claude Docs — Hooks guide).
Hãy so sánh:
- Viết "luôn chạy test trước khi commit" vào
CLAUDE.md→ Claude thường nhớ, đôi khi quên. - Đặt một hook chạy test → nó luôn chạy, không phụ thuộc tâm trạng của mô hình.
Đây là lý do Anthropic xếp hooks (cùng với permissions/managed settings) vào nhóm "guardrail thật", tách khỏi nhóm "chỉ dẫn" (Anthropic — Steering Claude Code). Hãy nghĩ về hooks như Git hooks cho agent của bạn.
Ngoài lệnh shell, một hook handler còn có thể là HTTP endpoint, MCP tool, một prompt gửi tới mô hình, hoặc một agent kiểm tra (Claude Docs — Hooks).
Các sự kiện hook (hook events)
Hook gắn vào các sự kiện đặt tên kiểu PascalCase. Bộ "kinh điển" bạn sẽ gặp nhiều nhất:
| Sự kiện | Kích hoạt khi | Chặn được? |
|---|---|---|
PreToolUse | Ngay trước khi một tool chạy | ✅ (chặn tool) |
PostToolUse | Sau khi tool trả kết quả | ❌ (việc đã xảy ra) |
UserPromptSubmit | Khi người dùng gửi prompt | ✅ (từ chối prompt) |
SessionStart | Đầu phiên | — (khởi tạo) |
SessionEnd | Cuối phiên | — |
Stop | Khi Claude định dừng lượt | ✅ (ngăn dừng) |
SubagentStop | Khi một subagent dừng | ✅ |
Notification | Khi Claude gửi thông báo | ❌ |
PreCompact | Trước khi nén ngữ cảnh | ✅ |
Tài liệu hiện tại còn liệt kê thêm nhiều sự kiện khác (danh sách mở rộng theo phiên bản), nên khi triển khai hãy kiểm tra đúng phiên bản Claude Code bạn dùng (Claude Docs — Hooks).
Có thể nhóm theo ba "nhịp": một lần mỗi phiên (SessionStart/SessionEnd), một lần mỗi lượt (UserPromptSubmit/Stop), và mỗi lần gọi tool trong vòng lặp agentic (PreToolUse/PostToolUse).
Cấu hình hook ở đâu
Hook được khai báo trong file JSON settings, với cấu trúc ba tầng lồng nhau: sự kiện → nhóm matcher → danh sách handler. Vị trí file quyết định phạm vi (Claude Docs — Hooks):
~/.claude/settings.json— mọi dự án (máy cá nhân)..claude/settings.json— một dự án, commit được vào repo (chia sẻ cho team)..claude/settings.local.json— một dự án, gitignored (riêng bạn).- Managed policy settings — toàn tổ chức (do admin đặt).
- Trong plugin:
hooks/hooks.json.
Cấu trúc điển hình:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "/path/to/lint-check.sh" }
]
}
]
}
}Matcher: cẩn thận với regex
matcher quyết định hook chạy cho tool nào:
"*","", hoặc bỏ trống = khớp tất cả.- Chỉ gồm chữ/số/
_/-(và|,,để liệt kê) = so khớp chuỗi chính xác. - Có ký tự khác = regex JavaScript không neo (unanchored).
Cạm bẫy kinh điển: Edit (dạng regex) sẽ khớp cả NotebookEdit. Muốn khớp trọn vẹn, bọc ^...$. Với tool MCP, matcher lọc theo tên dạng mcp__<server>__.*.
Input, output và exit code
Claude gửi cho hook một JSON mô tả ngữ cảnh (qua stdin với command, hoặc body POST với http). Các trường chung gồm session_id, transcript_path, cwd, permission_mode, hook_event_name; sự kiện tool có thêm tool_name, tool_input (Claude Docs — Hooks).
Exit code là cách điều khiển đơn giản nhất:
exit 0= thành công. Nếu hook in JSON ra stdout, Claude sẽ đọc JSON đó.exit 2= lỗi chặn (blocking). stdout bị bỏ qua; stderr được đưa lại cho Claude. Hệ quả tùy sự kiện:PreToolUsechặn tool call,UserPromptSubmittừ chối prompt,Stopngăn Claude dừng... (cònPostToolUse/Notificationthì không chặn được vì việc đã xảy ra).- Exit code khác = lỗi không chặn.
Muốn điều khiển tinh vi hơn exit code, hook có thể in JSON với các trường như continue, systemMessage, suppressOutput, decision/reason. Riêng PreToolUse hỗ trợ hookSpecificOutput.permissionDecision nhận allow/deny/ask/defer — cho phép "hỏi lại người dùng" trước khi tool chạy.
Hai ví dụ thực tế
(a) Tự lint/format sau khi sửa file — PostToolUse khớp Edit|Write, gọi script format. Không cần chặn gì, chỉ dọn dẹp sau mỗi lần sửa.
(b) Chặn lệnh nguy hiểm — PreToolUse khớp Bash, script đọc lệnh từ JSON và chặn nếu thấy rm:
#!/bin/bash
command=$(jq -r '.tool_input.command' < /dev/stdin)
if [[ "$command" == rm* ]]; then
echo "Bị chặn: lệnh rm không được phép" >&2
exit 2
fi
exit 0Đây chính là ví dụ "chặn rm -rf" trong sơ đồ bộ công cụ — và nó chắc chắn chặn, vì hook là tất định.
Best practices
- Giữ hook đầu phiên/đầu lượt nhanh.
UserPromptSubmitcó timeout (khoảng 30 giây) và làm nghẽn việc xử lý prompt — đừng làm việc nặng ở đây. - Việc dài thì chạy nền. Với
type: "command", đặt"async": trueđể Claude làm tiếp trong lúc hook (test suite, deploy) chạy nền. - Dùng biến đường dẫn.
${CLAUDE_PROJECT_DIR}và${CLAUDE_PLUGIN_ROOT}giúp script không phụ thuộc thư mục hiện hành. - Đặt đúng chỗ. Ngữ cảnh tĩnh nên ở
CLAUDE.md; hook dành cho hành động theo sự kiện.
Rủi ro bảo mật — đọc kỹ phần này
Đây là lớp quyền lực nhất và nguy hiểm nhất. Tài liệu chính thức ghi rõ: hook chạy lệnh shell tùy ý với toàn quyền của người dùng (Claude Docs — Hooks). Một hook độc hại hoặc viết ẩu có thể xóa file, rò rỉ dữ liệu, hoặc phá hệ thống.
Nguyên tắc an toàn (theo tài liệu):
- Validate và sanitize mọi input trước khi dùng.
- Luôn quote biến:
"$VAR", không để$VARtrần. - Chặn path traversal (
..), dùng đường dẫn tuyệt đối. - Tránh đụng file nhạy cảm:
.env,.git/, khóa/credential. - Ở cấp tổ chức, admin có thể ép chỉ cho phép hook đã duyệt chạy (managed hooks).
Với doanh nghiệp: hãy đối xử với hook như code hạ tầng — review, versioning, và giới hạn nguồn tin cậy, giống như bạn làm với Git hooks hay CI/CD.
Vài hiểu lầm cần tránh
- "Hook là gợi ý cho model." — Sai. Hook là bảo đảm tất định, khác hẳn skill/prompt.
- "exit 2 luôn chặn được." — Sai. Hệ quả tùy sự kiện;
PostToolUse/Notificationkhông chặn được. - "matcher
Editchỉ khớp Edit." — Sai. Đó là regex không neo, khớp cảNotebookEdit.
Tóm tắt & bước tiếp theo
Hooks là lớp kiểm soát: chạy tất định theo sự kiện vòng đời, cấu hình qua JSON settings (phạm vi tùy vị trí file), điều khiển bằng exit code (đặc biệt exit 2 để chặn), và là "guardrail thật" duy nhất bên cạnh permissions. Đổi lại quyền lực đó là trách nhiệm bảo mật cao.
Cho tới giờ, ta vẫn nói về một agent với bộ nhớ, kỹ năng và kiểm soát. Nhưng một agent không nên ôm mọi việc — giống một công ty cần nhiều vai. Bài 4 giới thiệu Subagents: chia việc cho một đội trợ lý AI, mỗi cái có ngữ cảnh và quyền riêng.
Nguồn tham khảo
- Claude Docs — Automate actions with hooks (hooks guide): https://docs.claude.com/en/docs/claude-code/hooks-guide
- Claude Docs — Hooks reference: https://docs.claude.com/en/docs/claude-code/hooks
- Anthropic — How to configure hooks: https://claude.com/blog/how-to-configure-hooks
- Claude Docs — Intercept and control agent behavior with hooks (Agent SDK): https://code.claude.com/docs/en/agent-sdk/hooks
- Anthropic — Steering Claude Code (chỉ dẫn vs guardrail): https://claude.com/blog/steering-claude-code-skills-hooks-rules-subagents-and-more
Bài 3/7 trong chuỗi "Bộ công cụ phát triển Agent với Claude". Nội dung diễn giải lại từ tài liệu gốc của Anthropic. Trước ← Bài 2: Agent Skills · Tiếp theo → Bài 4: Subagents.
