Đây là vấn đề với việc bảo Claude làm gì đó trong file CLAUDE.md: đó là một lời đề nghị, không phải một sự đảm bảo. Bạn có thể viết “luôn format sau khi sửa file” và Claude thường sẽ nghe theo. Thường. Nhưng trong một lần chạy dài mà bạn không theo dõi, “thường” là chưa đủ. Một hook khắc phục điều đó. Hook là code tất định (deterministic), chạy tại một điểm cố định trong vòng lặp, nên nó có thể đảm bảo hành vi thay vì chỉ hy vọng vào nó. Nó biến một quy tắc từ “Claude thường nghe theo” thành “Claude không thể bỏ qua”.
Đó là toàn bộ ý tưởng. Giờ hãy xem nó thực sự hoạt động ra sao.
Các hook event
Phần tiêu đề “Các hook event”Claude Code kích hoạt khoảng 30 hook event xuyên suốt một phiên làm việc. Bạn không cần biết hết tất cả. Có một nhóm nhỏ bạn sẽ dùng đi dùng lại, và chúng khớp với những điểm trong agentic loop mà bạn muốn can thiệp.
Đây là cách chúng nằm trong vòng lặp. Một phiên bắt đầu, prompt đi vào, tool được gọi, và lượt (turn) cuối cùng kết thúc. Mỗi thời điểm đó đều có một hook bạn có thể gắn code vào.
Những cái đáng biết nhất:
- PreToolUse kích hoạt trước một lệnh gọi tool. Đây là “nguyên thủy thực thi” (enforcement primitive) của bạn - cái duy nhất có thể chặn một việc trước khi nó xảy ra.
- PostToolUse kích hoạt sau khi một lệnh gọi tool thành công. Đây thường là nơi đặt auto-format hoặc auto-lint.
- Stop kích hoạt khi Claude muốn kết thúc lượt của nó. Bạn có thể từ chối và nói “chưa xong đâu” nếu một điều kiện nào đó chưa được đáp ứng. Có một SubagentStop tương ứng khi một sub-agent hoàn tất.
- PreCompact và PostCompact kích hoạt trước và sau khi compaction.
- InstructionsLoaded kích hoạt khi một file CLAUDE.md hoặc file quy tắc được nạp. Hữu ích để audit xem thực sự cái gì đã lọt vào ngữ cảnh.
- SessionStart kích hoạt lúc bắt đầu và chuẩn bị môi trường. Dùng nguồn (source)
startupnếu chỉ muốn nó chạy khi khởi động mới hoàn toàn.
Một điều dễ khiến người ta nhầm lẫn: để bơm lại ngữ cảnh sau compaction, đừng dùng PostCompact. Hãy dùng SessionStart với matcher compact. Đó mới là cái thực sự đưa được output của nó trở lại hội thoại.
PreToolUse: trả về quyết định dưới dạng JSON
Phần tiêu đề “PreToolUse: trả về quyết định dưới dạng JSON”PreToolUse là nơi có sức mạnh thực sự, vì nó có thể chặn một lệnh gọi tool trước khi nó chạy. Cách bạn “nói chuyện” ngược lại với Claude là in ra JSON và exit với mã 0. Field quan trọng nhất là permissionDecision, nhận một trong ba giá trị:
allow- cho lệnh gọi đi qua.deny- chặn lệnh gọi.ask- trả lại cho người dùng tự quyết định.
Về mặt kỹ thuật còn có giá trị thứ tư, defer, nhưng nó chỉ áp dụng cho các lần chạy -p không tương tác, nơi một tiến trình gọi tạm dừng tool rồi tiếp tục sau. Bạn hiếm khi cần dùng tới nó.
Hình dạng của nó trông như sau:
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "...", "updatedInput": { "command": "..." } }}Chú ý updatedInput. Thay vì chặn một lệnh gọi, bạn có thể viết lại nó. Đó là cách bạn có thể xoá (redact) một secret ra khỏi một lệnh bash mà vẫn cho nó chạy. Một điểm cần lưu ý: updatedInput thay thế toàn bộ object input, nên bạn phải gửi lại tất cả các field bạn không thay đổi, nếu không chúng sẽ bị mất.
Exit code, cho các hook không trả về JSON
Phần tiêu đề “Exit code, cho các hook không trả về JSON”Không phải hook nào cũng cần “nói” JSON. Với các hook đơn giản hơn, exit code đã đủ làm nhiệm vụ. Có ba con số quan trọng.
- 0 là thành công. Nếu standard out là JSON, Claude sẽ parse nó. Văn bản thuần bị bỏ qua ở đa số event, nhưng riêng với SessionStart, UserPromptSubmit, và UserPromptExpansion, văn bản thuần sẽ được thêm vào ngữ cảnh. Đó chính xác là cách một hook “bảo toàn trạng thái” (state-preserver) hoạt động.
- 2 là lỗi chặn (blocking error). Standard error được đưa ngược lại cho Claude làm ngữ cảnh. Đây là mã exit chặn ở gần như mọi nơi.
- Bất kỳ giá trị nào khác là không-chặn (non-blocking). Standard error được ghi log, và Claude tiếp tục.
Cái hay khiến người ta mắc bẫy là exit code 1. Nó cảm giác như một lỗi, nhưng nó không chặn gì cả. Claude vẫn chạy lệnh đó bình thường. Vậy nên nếu bạn muốn chặn một thứ gì đó, hãy exit 2, không phải 1.
Vài điểm cần lưu ý thêm. Exit 2 thậm chí có thể chặn cả Stop, đó là cách bạn báo cho Claude biết nó chưa xong. Nhưng PostToolUse kích hoạt sau khi tool đã chạy rồi, nên chặn ở đó là quá muộn để ngăn lệnh gọi, dù nó vẫn có thể đưa văn bản trở lại cho Claude. Và một số event bỏ qua hoàn toàn cơ chế chặn, như Notification và SessionStart. Chúng sẽ hiển thị standard error của bạn rồi tiếp tục bất kể thế nào.
Một guardrail thực tế: redact thay vì chặn
Phần tiêu đề “Một guardrail thực tế: redact thay vì chặn”Hãy gộp lại bằng một ví dụ thực tế. Giả sử bạn muốn một guardrail PreToolUse trên tool Bash. Matcher chọn tool cần theo dõi, và một mệnh đề if tùy chọn có thể thu hẹp xuống một lệnh cụ thể.
Cách hiển nhiên nhất là trả về deny và chặn một lệnh gọi nguy hiểm. Điều đó tốt. Nhưng cách ít người biết hơn và thú vị hơn là trả về updatedInput để viết lại lệnh gọi. Đó là cách bạn xoá một secret ra khỏi một lệnh mà vẫn cho nó chạy, thay vì chỉ đơn giản từ chối.
Đây là ví dụ thực tế. Claude được yêu cầu chạy một lệnh có chứa một secret trông như đang hoạt động (live). Hook chặn nó lại, phát hiện pattern sk_live_, và thay nó bằng một placeholder trước khi lệnh thực sự được thực thi.
Lệnh vẫn chạy. Công việc vẫn được hoàn thành. Nhưng secret không bao giờ lọt qua. Đó là khác biệt giữa chặn và redact, và đó chính là kiểu việc mà một hook có thể thực thi mỗi lần, không sai sót.
Bảo toàn trạng thái qua một lần compact
Phần tiêu đề “Bảo toàn trạng thái qua một lần compact”Một pattern nữa đáng thiết lập. Khi Claude compact một hội thoại dài, nó bỏ đi rất nhiều chi tiết. Một SessionStart hook với matcher compact chạy ngay sau khi compaction hoàn tất. Hãy cho nó in ra một bản tóm tắt ngắn về các file bạn đang làm việc. Bản tóm tắt đó quay lại ngữ cảnh, nên Claude tiếp tục từ chỗ nó dừng lại thay vì bắt đầu lại từ số 0.
Kết luận
Phần tiêu đề “Kết luận”Hooks biến một quy tắc Claude thường tuân theo thành một quy tắc nó luôn luôn tuân theo. Đừng chỉ dừng ở auto-format: canh gác tool bằng PreToolUse, chốt chặn lượt (turn) bằng Stop, và bảo toàn trạng thái qua một lần compact. Việc thiết lập tốn chút công sức ban đầu, nhưng nó đền đáp ngay lần đầu tiên bắt được lỗi trong một lần chạy mà bạn còn chẳng theo dõi.
Xem thêm: Hooks · Tài liệu tham chiếu hooks
lượt xem