Hooks trong Claude Code hoạt động thế nào?
Hooks biến những chỉ dẫn 'nhớ làm giúp tôi' thành đảm bảo chắc chắn. Bài này mổ xẻ vòng đời hook, cách cấu hình, luồng vào/ra bằng exit code & JSON, kèm ví dụ thật.
Khi làm việc với Claude Code, có những thứ bạn muốn luôn luôn xảy ra: format code sau mỗi lần sửa, chặn tuyệt đối việc đụng vào .env, ghi log mọi câu lệnh shell. Nếu chỉ dặn model “nhớ format nhé”, bạn đang phó thác cho phán đoán của một hệ thống xác suất — nó có thể quên. Hooks giải quyết đúng chỗ đó: chúng là các lệnh chạy tự động, tất định tại những mốc cố định trong vòng đời của Claude Code.
Vòng đời: hook cắm vào đâu?
Claude Code chạy theo một agentic loop — nhận prompt, suy nghĩ, gọi tool, lặp lại tới khi xong. Hook cắm vào giữa các mốc đó. Đây là những điểm bạn dùng nhiều nhất:
flowchart TD
A["Bạn gõ prompt"] --> B["Claude chọn công cụ"]
B --> C["Gọi tool · Bash / Edit"]
C --> D["Lặp agentic loop tới khi xong"]
D --> E["Claude dừng"]
U(["UserPromptSubmit"]) -.-> A
P(["PreToolUse — chặn / duyệt / sửa input"]) -.-> C
C -.-> Q(["PostToolUse — format / inject"])
S(["Stop — ép làm tiếp nếu chưa đạt"]) -.-> E
Bốn hook hay dùng nhất quanh agentic loop — PreToolUse chạy TRƯỚC khi tool thực thi (chặn được); PostToolUse chạy SAU (không undo được).
Bốn cái đó mới là phần bạn chạm vào hằng ngày. Tài liệu chính thức hiện liệt kê khoảng ba chục event1, phủ gần như mọi mốc trong một phiên làm việc: SessionStart / SessionEnd (đầu và cuối phiên), Notification (khi Claude chờ bạn phản hồi), PreCompact / PostCompact (quanh lúc nén context), SubagentStop, PermissionRequest, cho tới cả FileChanged hay WorktreeCreate. Không cần thuộc hết; chỉ cần biết rằng chỗ nào bạn muốn chen vào thì nhiều khả năng đã có sẵn một mốc ở đó.
Cấu hình ở đâu?
Hooks khai báo trong settings.json2, theo thứ tự ưu tiên từ rộng đến hẹp:
| Vị trí | Phạm vi | Commit vào repo? |
|---|---|---|
~/.claude/settings.json | Mọi project (máy bạn) | Không |
.claude/settings.json | Riêng project này | Có — chia sẻ cho team |
.claude/settings.local.json | Riêng project, riêng bạn | Không (bị gitignore) |
Cấu trúc JSON gồm: tên event → danh sách nhóm, mỗi nhóm có matcher (lọc theo tên tool) và mảng hooks:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard.sh",
"timeout": 30
}
]
}
]
}
}
Matcher lọc tool nào kích hoạt hook: "Bash" (khớp đúng tên), "Edit|Write" (hoặc), "mcp__.*" (regex cho MCP tools), hoặc bỏ trống / "*" để khớp tất cả. Với các event không phải tool (như SessionStart), matcher lọc theo lý do — ví dụ "compact", "startup", "resume".
Luồng vào/ra: hook “nói chuyện” với Claude ra sao?
Đây là phần cốt lõi. Mỗi khi kích hoạt, hook nhận một JSON qua stdin, rồi trả quyền điều khiển lại bằng exit code và (tùy chọn) JSON qua stdout.
Input mẫu cho một PreToolUse trên Bash:
{
"session_id": "session-xyz",
"cwd": "/Users/namnn/project",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "npm test" }
}
Exit code quyết định hành vi:
| Exit code | Ý nghĩa |
|---|---|
| 0 | OK. Với UserPromptSubmit/SessionStart, stdout được nối vào context của Claude. |
| 2 | Chặn. Hành động bị chặn (nếu event chặn được); nội dung stderr được đưa lại cho Claude làm phản hồi. |
| Khác | Lỗi không chặn — hành động vẫn tiếp tục, lỗi hiện trong transcript. |
Khi cần điều khiển tinh vi hơn exit code, in JSON có cấu trúc ra stdout (với exit 0). Ví dụ, một PreToolUse từ chối kèm lý do:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Dùng 'git push --force-with-lease' thay cho '--force'"
}
}
permissionDecision nhận allow, deny hoặc ask1. Còn một giá trị thứ tư là defer, nhưng nó chỉ dùng được ở chế độ non-interactive với cờ -p: hook thoát process mà vẫn giữ nguyên tool call, để một lớp Agent SDK bọc ngoài thu thập thêm dữ liệu rồi chạy tiếp. Trong phiên tương tác hằng ngày, bạn hầu như chỉ đụng tới ba giá trị đầu.
Thậm chí bạn có thể viết lại input của tool bằng updatedInput trước khi nó chạy, ví dụ tự đổi --force thành --force-with-lease.
Khi nhiều hook cùng khớp một tool, chúng không tranh nhau: Claude Code gộp kết quả và lấy câu trả lời ngặt nhất, theo thứ tự deny → defer → ask → allow. Vì vậy thêm một hook chỉ có thể làm mọi thứ chặt hơn, không bao giờ lỏng đi.
Trọn vẹn một lượt PreToolUse chặn rồi cho Claude thử lại trông như sau:
sequenceDiagram
participant C as Claude
participant H as PreToolUse hook
participant T as Tool · Bash
C->>H: muốn chạy "git push --force"
H-->>C: deny + lý do
Note over C: Claude tự điều chỉnh
C->>H: thử "git push --force-with-lease"
H->>T: allow
T-->>C: kết quả trả về
PreToolUsechạy trước nên chặn hoặc sửa được input;PostToolUsechạy sau nên chỉ transform kết quả hoặc chặn lượt model kế tiếp, chứ không undo được việc đã làm.
Ba ví dụ thật
1. Tự format sau mỗi lần sửa file
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
Sau khi Claude sửa file, hook lấy file_path từ JSON input rồi chạy Prettier. Im lặng, tất định, không bao giờ quên.
2. Chặn cứng việc đụng vào file nhạy cảm
.claude/hooks/protect.sh:
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
for p in ".env" "package-lock.json" ".git/"; do
if [[ "$FILE" == *"$p"* ]]; then
echo "Chặn: $FILE trùng mẫu bảo vệ '$p'" >&2
exit 2
fi
done
exit 0
Trả exit 2 → tool bị chặn, và Claude nhận đúng lý do để tự điều chỉnh. Quan trọng: hook deny chặn được kể cả ở chế độ bypassPermissions — đó là hàng rào cứng, không lách được.
3. Báo desktop khi Claude cần bạn
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt|idle_prompt",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude cần bạn\" with title \"Claude Code\"'"
}
]
}
]
}
}
Rời máy đi pha cà phê mà không sợ bỏ lỡ prompt xin quyền.
Vài cạm bẫy nên nhớ
exit 1không chặn được gì. Chỉexit 2mới chặn. Docs nói thẳng rằng Claude Code coi exit code 1 là lỗi không chặn và vẫn tiến hành hành động, dù 1 mới là mã lỗi quy ước của Unix1. Quen tay viếtexit 1để từ chối thì hook của bạn chỉ ghi một dòng lỗi vào transcript rồi để mọi thứ chạy tiếp.- Hook chạy bằng quyền của bạn — với SSH key, token, quyền ghi file của bạn. Chỉ dùng hook từ nguồn tin cậy, đọc script trước khi bật.
- Đừng in secret ra stdout/stderr — Claude đọc được nội dung đó như một system message.
PostToolUsekhông undo được — cần chặn thì phải dùngPreToolUse.- Cẩn thận shell injection — dùng
jqđể trích & quote giá trị, đừng nội suy thẳngtool_inputvào chuỗi lệnh. - Output từ
.bashrc/.zshrcin vô điều kiện sẽ làm hỏng JSON của hook — bọc trong kiểm tra shell tương tác ([[ $- == *i* ]]).
Hook siết được, nhưng không nới được
Quay lại cái hook chặn .env ở trên. Điều khiến nó đáng tin không nằm ở mấy dòng script, mà ở chỗ PreToolUse chạy trước mọi bước kiểm tra permission mode, nên một hook trả deny vẫn chặn được tool kể cả khi phiên làm việc mở bằng --dangerously-skip-permissions1. Không ai lách nó bằng cách đổi permission mode của mình được, và đó là lý do hook đủ sức mang những luật mà bạn muốn cả team phải theo.
Chiều ngược lại thì không đúng, và đây là chỗ dễ hiểu nhầm. Một hook trả allow không xoá được luật deny đã khai trong settings; nó chỉ bỏ qua bước hỏi cho những gì settings vốn đã cho phép. Nói theo tài liệu, hook siết chặt được hạn chế chứ không nới lỏng quá giới hạn mà permission rules đặt ra1.
Sự bất đối xứng ấy không phải một thiếu sót còn dang dở, nó chính là thứ làm hook đáng dùng. Một cơ chế vừa siết vừa nới được thì chẳng bảo đảm nổi điều gì, vì luật nào cũng có thể bị chính một hook khác gỡ ra. Còn khi công cụ chỉ đi được một chiều, bạn đọc một file settings.json là biết giới hạn trên của những gì Claude Code làm được trong repo này, không cần suy đoán.
Nếu trong repo của bạn đang có một quy tắc mà mọi người vẫn “nhớ thì làm”, chẳng hạn chạy migration trước khi test hay không bao giờ commit thẳng lên main, thì đó là ứng viên đầu tiên nên chuyển thành hook. Câu hỏi đáng hỏi không phải là Claude có nhớ làm việc đó không, mà là nếu nó quên thì cái gì chặn lại?
Tài liệu tham khảo
- Anthropic (2025). Hooks reference — Claude Code. code.claude.com/docs/en/hooks
- Anthropic (2025). Automate actions with hooks — Claude Code. code.claude.com/docs/en/hooks-guide