Hooks trong Claude Code: khi nào prompt không đủ
Vì sao chỉ dẫn trong prompt vẫn có lúc sai còn hook thì không: PreToolUse là gì, ví dụ PostToolUse, bảng chọn prompt hay hook và bẫy trong đề CCAR-F.
System prompt ghi "không bao giờ hoàn tiền quá $500 khi chưa có người duyệt" thì phần lớn thời gian Claude làm đúng. Nhưng với một quy định tuân thủ, "phần lớn thời gian" nghĩa là vẫn có tỉ lệ sai. Hooks trong Claude Code và Agent SDK chạy code của bạn tại những điểm cố định trong agent loop, nên quy tắc được giữ ở mọi lần gọi tool. Bài này giải thích khi nào đề CCAR-F muốn bạn chọn hook thay vì sửa prompt, kèm ví dụ PreToolUse chạy được và bảng so sánh.
Prompt là xác suất, hook là tất định
Exam guide nói thẳng điều này trong task statement 1.4 và 1.5: khi cần tuân thủ tuyệt đối, ví dụ xác minh danh tính trước thao tác tài chính, thì chỉ dùng prompt sẽ luôn có tỉ lệ thất bại khác 0.
Một chỉ dẫn trong prompt chỉ là một yếu tố model cân nhắc cùng mọi thứ khác trong context. Hội thoại dài, khách hàng nài nỉ, hay một tool result khó hiểu đều có thể kéo model đi hướng khác. Hook thì là hàm do ứng dụng của bạn chạy mỗi khi sự kiện khớp xảy ra. Nó không cân nhắc gì cả: điều kiện đúng thì chặn.
Cách chia việc:
- Prompt cho sở thích và phán đoán: giọng văn, khi nào hỏi lại cho rõ, thử tool nào trước.
- Hook và programmatic gate cho quy tắc doanh nghiệp phải đảm bảo: hạn mức hoàn tiền, bước xác minh bắt buộc, chuẩn định dạng dữ liệu.
PreToolUse là gì: chặn, chuyển hướng, ép thứ tự
PreToolUse chạy sau khi Claude yêu cầu gọi tool và trước khi tool thực sự chạy. Hook có thể cho phép, từ chối kèm lý do (Claude đọc được lý do này), hoặc sửa input.
Hai tình huống chuẩn trong guide:
process_refundtrên $500 không được chạy, agent phải gọiescalate_to_human.process_refundkhông được chạy cho đến khiget_customertrả về customer ID đã xác minh.
Bản tối giản bằng Agent SDK (Python), giả sử các tool hỗ trợ đến từ MCP server tên support, nên tên tool có dạng mcp__support__<tool>:
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher
verified = set() # các session_id đã qua get_customer
async def record_verification(input_data, tool_use_id, context):
# PostToolUse trên get_customer: ghi nhận session này đã xác minh khách
if "verified" in str(input_data.get("tool_response", "")):
verified.add(input_data["session_id"])
return {}
def deny(reason):
return {"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": reason,
}}
async def refund_policy(input_data, tool_use_id, context):
if input_data["session_id"] not in verified:
return deny("Call get_customer and verify the customer before any refund.")
if input_data["tool_input"].get("amount", 0) > 500:
return deny("Refunds over $500 need a human. Call escalate_to_human "
"with customer ID, root cause, amount and recommended action.")
return {} # không phản đối, luồng bình thường tiếp tục
options = ClaudeAgentOptions(hooks={
"PreToolUse": [HookMatcher(matcher="mcp__support__process_refund",
hooks=[refund_policy])],
"PostToolUse": [HookMatcher(matcher="mcp__support__get_customer",
hooks=[record_verification])],
})
Đoạn "verified" in str(...) chỉ thay cho việc parse response thật của get_customer. Phần cần nhớ là cấu trúc: lý do từ chối được trả về cho Claude, và Claude đi theo đường thay thế (xác minh trước, hoặc escalate) thay vì thất bại im lặng. Đó chính là mẫu "chặn và chuyển hướng" trong guide.
Trong Claude Code, hook tương tự là một command trong .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__support__process_refund",
"hooks": [{ "type": "command", "command": "./.claude/hooks/refund-policy.sh" }]
}
]
}
}
Script nhận JSON sự kiện (có tool_name, tool_input) qua stdin. Exit code 2 chặn lời gọi và gửi stderr cho Claude làm lý do. Exit 0 kèm JSON trên stdout cho phép trả permissionDecision (allow, deny, ask, bản mới có thêm defer). Các exit code khác chỉ là lỗi không chặn, tool vẫn chạy. Nghĩa là script chính sách mà crash thì không chặn được gì, điểm này hay bị bỏ sót.
PostToolUse: chuẩn hóa output trước khi model suy luận
Mẫu thứ hai trong task statement 1.5 đi theo chiều ngược lại. PostToolUse chạy sau khi tool chạy thành công, nhận tool_response, và có thể đổi thứ Claude nhìn thấy.
Tình huống đề hay ra: ba MCP tool trả ngày theo Unix seconds, ISO 8601 và "10/03/2026", trạng thái đơn hàng thì nơi là mã số, nơi là chữ. Dặn Claude trong prompt "chú ý định dạng ngày" vẫn là xác suất. Một PostToolUse hook đổi mọi timestamp sang ISO 8601 và map mã trạng thái thành tên sẽ làm dữ liệu nhất quán trước khi model đọc.
Ở bản Claude Code và Agent SDK hiện tại, hook trả kết quả đã làm sạch trong hookSpecificOutput.updatedToolOutput, phải đúng shape output của tool (field cũ updatedMCPToolOutput chỉ áp dụng cho MCP tool). Lúc này tool đã chạy xong rồi. Muốn chặn hay sửa lời gọi thì dùng PreToolUse.
flowchart LR
A[Claude yêu cầu tool] --> B{PreToolUse hook}
B -- deny + lý do --> A
B -- allow --> C[Tool chạy]
C --> D[PostToolUse chuẩn hóa output]
D --> E[Claude suy luận trên dữ liệu sạch]
Bảng chọn: prompt hay hook
| Yêu cầu | Chỉ dẫn trong prompt | Hook / programmatic gate |
|---|---|---|
| Hạn mức hoàn tiền, giới hạn chi, thao tác không đảo ngược | Không đủ | Có, PreToolUse deny + chuyển hướng |
| "Xác minh danh tính trước thao tác tài chính" | Không đủ | Có, prerequisite gate |
| Định dạng dữ liệu nhất quán giữa các MCP tool | Không chắc chắn | Có, PostToolUse chuẩn hóa |
| Giọng văn, sự đồng cảm, cách diễn đạt | Có | Thừa |
| Hỏi lại khi yêu cầu mơ hồ | Có | Hook không đánh giá được độ mơ hồ |
| Ưu tiên tool này hơn tool tương tự | Có, kèm tool description tốt hơn | Thường không cần |
Câu hỏi để quyết định: sai một lần có chấp nhận được không? Nếu một lần vi phạm là sự cố tuân thủ hoặc mất tiền, hãy đưa quy tắc vào code.
Thử sức
Một agent hỗ trợ khách hàng xây bằng Agent SDK thỉnh thoảng hoàn tiền vượt hạn mức $500, dù system prompt đã in đậm hạn mức và nhắc lại ở cuối. Cách khắc phục hiệu quả nhất là gì?
A. Đưa hạn mức lên đầu system prompt và thêm ba ví dụ few-shot về escalate đúng cách.
B. Thêm PreToolUse hook trên process_refund, từ chối số tiền trên $500 và bảo agent gọi escalate_to_human.
C. Giảm temperature để agent tuân thủ chỉ dẫn ổn định hơn.
D. Thêm PostToolUse hook ghi log mọi khoản hoàn tiền trên $500 để audit hằng tuần.
Đáp án: B. Quy tắc cần được đảm bảo, và PreToolUse hook chặn trước khi tiền đi ra, đồng thời chuyển agent sang luồng escalate.
- A là "củng cố prompt". Có thể giảm tỉ lệ sai nhưng vẫn là xác suất, đây đúng là cái bẫy câu hỏi muốn kiểm tra.
- C chỉ ảnh hưởng độ ngẫu nhiên khi sampling, không liên quan tuân thủ chính sách. Không có gì chặn lệnh hoàn tiền sai.
- D đúng cơ chế nhưng sai thời điểm. PostToolUse chạy sau khi tiền đã hoàn, nên chỉ phát hiện được vi phạm.
Ghi nhớ chính
- Chỉ dẫn trong prompt luôn có tỉ lệ thất bại khác 0. Quy tắc cần tuân thủ tuyệt đối phải nằm trong hook hoặc programmatic gate.
- PreToolUse chặn lời gọi đi ra: deny kèm lý do, chuyển hướng sang
escalate_to_human, hoặc épget_customerphải chạy trước. - PostToolUse biến đổi tool result, ví dụ timestamp nhiều định dạng, trước khi model suy luận.
- Với command hook, exit
2mới chặn. Các mã khác 0 còn lại thì không. - Khi một đáp án nói "củng cố system prompt" cho quy tắc tuyệt đối không được vi phạm, hãy tìm đáp án dùng hook.
Câu hỏi thường gặp
Cùng tên sự kiện, cùng format output. Hooks trong Claude Code thường là shell command cấu hình trong settings.json. Hooks trong Agent SDK là callback bạn truyền vào options.hooks, và SDK cũng có thể nạp command hook từ file settings.
Có. Trả updatedInput bên trong hookSpecificOutput để thay tham số trước khi tool chạy, ví dụ giới hạn một giá trị hoặc đổi đường dẫn.
Không nên. Giữ chính sách trong prompt để Claude lên kế hoạch theo nó và giải thích được cho khách. Hook là lớp chặn cuối, giữ cho chính sách đúng cả khi model sai.
Đọc thêm: Claude agentic loop: workflow hay agent, escalation và bàn giao cho người, và MCP tools, resources, prompts. Luyện câu hỏi Domain 1 để quen dạng câu hỏi về hook và enforcement.