Đến nội dung chính
Agentic architecture

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.

Đội ngũ CertGrove · 6 phút đọc

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:

  1. process_refund trên $500 không được chạy, agent phải gọi escalate_to_human.
  2. process_refund không được chạy cho đến khi get_customer trả 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ầuChỉ dẫn trong promptHook / programmatic gate
Hạn mức hoàn tiền, giới hạn chi, thao tác không đảo ngượcKhô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 toolKhông chắc chắnCó, PostToolUse chuẩn hóa
Giọng văn, sự đồng cảm, cách diễn đạtCó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ơnThườ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 ép get_customer phả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 2 mớ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

Đội ngũ CertGroveViết ghi chú ôn thi song ngữ cho developer Việt Nam chuẩn bị Claude Certified Architect — Foundations.