Hooks trong Claude Code 2026: hướng dẫn thiết lập từng bước và 7 ví dụ dùng ngay
Markdown7 hooks dùng ngay cho .claude/settings.json, sẵn sàng sao chép và dán: bảo vệ .env, prettier, chặn force-push lên main, em dash và máy chủ dev. 4 loại handler và 31 sự kiện.
Đọc toàn bộ bài viết
Hooks trong Claude Code là các lệnh shell, lệnh gọi HTTP hoặc bước kiểm tra LLM ngắn chạy tự động trước hoặc sau một hành động của Claude. Chúng thêm kiểm vào sự kiện công cụ cụ thể, định dạng mã sau sửa và chèn ngữ cảnh lúc mở phiên. PreToolUse khớp có thể từ chối lời gọi đó; regex này là guard tiện dụng, không ranh giới bảo mật qua mọi đường tệp/Git. Tiếp theo là phần phân tích 4 loại handler, 31 sự kiện vòng đời, bài kiểm tra “khi nào một quy tắc nên nằm trong hook”, 7 hook sẵn sàng sao chép vào .claude/settings.json và 3 cạm bẫy tôi từng gặp.
Tài liệu Ailoot: API, khóa và thiết lập.
Hooks trong Claude Code là gì?
Điểm chính
Hook là một lệnh shell, lệnh gọi HTTP, prompt LLM ngắn hoặc subagent mà Claude Code chạy để phản hồi một sự kiện vòng đời cụ thể. Khác với hướng dẫn trong CLAUDE.md, vốn có thể bị mô hình bỏ qua hoặc quên, hook luôn chạy theo cùng một cách: đó là mã, không phải lời gợi ý.
Suốt nửa năm, tôi giữ quy tắc “không bao giờ chỉnh sửa trực tiếp .env” trong CLAUDE.md. Mỗi tháng Claude lại vi phạm một lần. Không phải cố ý; đơn giản là đến cuối phiên dài, nó quên mất. Có lần một token thừa lọt vào .env; lần khác, một biến trùng tên ghi đè biến đang được dùng.
Tôi chuyển quy tắc sang hook. Khi công cụ sửa trực tiếp khớp cung cấp đường .env, hook cấu hình từ chối lời gọi đó. Bash redirection, script, MCP và đường khác cần kiểm soát riêng; ví dụ không làm tệp bất biến.
Hooks là các lệnh shell, điểm cuối HTTP hoặc prompt LLM do bạn định nghĩa và tự động chạy tại những thời điểm cụ thể trong vòng đời Claude Code.
Nói cách khác, hook là lệnh của bạn mà Claude Code chạy khi một sự kiện cụ thể xảy ra. Mỗi lần chỉnh sửa tệp, khởi chạy Bash, bắt đầu phiên hoặc subagent hoàn tất đều là một sự kiện mà bạn có thể gắn hook vào.
Điều này quan trọng với người viết mã theo kiểu vibe coding vì:
- Bảo vệ các tệp chứa bí mật khỏi bị chỉnh sửa ngoài ý muốn.
- Tự động chạy prettier/eslint sau mỗi lần tạo mã.
- Chặn
git push --forcelên main nếu chưa được cho phép rõ ràng. - Chèn ngữ cảnh mới vào đầu mỗi phiên.
- Ghi nhật ký hoạt động của Claude để kiểm tra.
CLAUDE.md xử lý ngữ cảnh. Hooks xử lý hành động. Đây là hai việc khác nhau, và nhầm lẫn chúng là lỗi phổ biến nhất của người mới.
Hooks khác CLAUDE.md và skills như thế nào?
Điểm chính
CLAUDE.md là văn bản mà mô hình đọc khi bắt đầu phiên và cố gắng làm theo. Hook là lệnh shell do tiến trình Claude Code chạy để phản hồi một sự kiện, độc lập với mô hình. Skill là một gói hướng dẫn có tên mà Claude có thể gọi bằng công cụ Skill, giống như một hàm. Cả ba giải quyết các loại nhiệm vụ khác nhau.
Tài liệu của Anthropic giải thích rõ sự khác biệt giữa CLAUDE.md và hooks:
Nội dung CLAUDE.md được gửi dưới dạng tin nhắn người dùng sau system prompt, không phải một phần của system prompt. Claude đọc và cố gắng làm theo, nhưng không có bảo đảm tuân thủ nghiêm ngặt.
Nói cách khác: nội dung CLAUDE.md đến với Claude dưới dạng tin nhắn đầu tiên từ người dùng, không phải chỉ thị hệ thống. Mô hình sẽ đọc và cố gắng làm theo. Nhưng không có gì bảo đảm, nhất là về cuối phiên dài, khi ngữ cảnh tăng lên và các hướng dẫn cũ bị đẩy ra ngoài.
Hook hoạt động khác. Nó nằm trong settings.json, không phụ thuộc mô hình và được tiến trình Claude Code chạy khi có sự kiện cụ thể: PreToolUse, PostToolUse, SessionStart và các sự kiện khác. Lệnh chạy trước khi Claude nhận kết quả và có thể chặn hành động thực sự (exit 2) hoặc sửa tệp sau khi chỉnh sửa (exit 0).
So sánh:
| Tính năng | CLAUDE.md | Hook | Skill |
|---|---|---|---|
| Nằm ở đâu | CLAUDE.md trong thư mục gốc dự án | .claude/settings.json | Thư mục .claude/skills/ |
| Ai chạy | Mô hình đọc như một phần tin nhắn người dùng | Tiến trình Claude Code khi có sự kiện | Mô hình thông qua công cụ Skill |
| Có bảo đảm thực thi không | Không (cố gắng hết sức) | Hook command cấu hình đúng, khớp sẽ chạy mã; lỗi và hành vi exit tùy sự kiện | Qua một lần gọi mô hình (có cấp quyền, nhưng không ở cấp shell) |
| Dùng để làm gì | Ngữ cảnh dự án, giọng điệu, phong cách, định tuyến | Hạn chế, định dạng, kiểm tra một số tệp cụ thể | Quy trình nhiều bước phức tạp có tham số |
| Chạy khi nào | Mỗi tin nhắn của mô hình trong phiên | Khi có sự kiện vòng đời | Khi mô hình gọi |
| Có thể chặn hành động không | Không | Có (exit 2) | Không |
Mẫu CLAUDE.md đầy đủ và 6 quy tắc để duy trì tệp này nằm trong hướng dẫn Cách cấu hình CLAUDE.md năm 2026: mẫu dùng ngay và 6 quy tắc. Tìm hiểu về skills như các năng lực có tên trong Claude Code Skills: Cách xây dựng thư viện của riêng bạn.
Hook là cấp thấp nhất: không có mô hình, không diễn giải, chỉ có shell.
Hooks là một trong các công cụ context engineering, cùng với Plan Mode và /compact.
4 loại hook: Command, HTTP, Prompt, Agent — nên chọn loại nào?
Điểm chính
Claude Code có 4 loại handler. Command là tập lệnh shell, loại phổ biến nhất. HTTP gửi yêu cầu POST đến URL của bạn, phù hợp với nhóm và tích hợp. Prompt gọi Haiku trong thời gian ngắn để kiểm tra ngữ nghĩa. Agent tạo subagent có quyền truy cập công cụ để kiểm tra sâu. Người mới cần Command và Prompt; chúng đáp ứng 90% nhiệm vụ.
| Loại | Chức năng | Khi nào nên chọn | Độ phức tạp |
|---|---|---|---|
| Command | Chạy lệnh shell, nhận JSON qua stdin và phản hồi bằng mã thoát cùng stdout | 90% trường hợp: kiểm tra đường dẫn, định dạng, grep nội dung, lsof, thao tác git | thấp |
| HTTP | Gửi yêu cầu POST đến URL của bạn với JSON body và nhận JSON phản hồi | Khi quy tắc thuộc về nhóm/CI: xác thực tập trung, bảng trạng thái dùng chung, kiểm tra từ xa | trung bình (cần máy chủ) |
| Prompt | Gửi prompt ngắn đến Haiku (theo mặc định) và nhận yes/no để đánh giá ngữ nghĩa | Khi grep chưa đủ: “đánh giá xem văn bản này có mang giọng điệu doanh nghiệp không” | thấp |
| Agent | Tạo subagent có quyền truy cập Read/Grep/Glob để thực hiện kiểm tra nhiều bước | Xác thực nặng: “tìm mọi tệp đã thay đổi và kiểm tra xem có bài kiểm tra tương ứng không” | cao |
Lỗi phổ biến nhất là cố dùng Command cho việc cần Prompt. Ví dụ, “không dùng em dash trong tệp .md” là tác vụ grep, nên Command rất phù hợp. Nhưng “đừng viết với giọng điệu doanh nghiệp” là vấn đề ngữ nghĩa, nên cần Prompt.
Nếu mới bắt đầu, hãy dùng Command. Cả 7 hook dùng ngay bên dưới đều là Command. HTTP/Prompt/Agent có thể dùng sau, khi bạn sẵn sàng.
settings.json nằm ở đâu và có 4 cấp cấu hình nào?
Điểm chính
Claude Code tìm cấu hình ở 4 nơi: cấp doanh nghiệp (triển khai qua MDM), cấp người dùng (~/.claude/settings.json), cấp dự án (.claude/settings.json ở thư mục gốc repository) và cấp local (.claude/settings.local.json, không được đưa vào git). Cả 4 tệp được hợp nhất khi bắt đầu phiên, trong đó cấp thấp hơn được ưu tiên.
| Cấp | Đường dẫn | Áp dụng khi nào | Có trong git? |
|---|---|---|---|
| Doanh nghiệp | /Library/Application Support/ClaudeCode/settings.json (Mac), /etc/claude-code/settings.json (Linux) | Cho cả nhóm thông qua MDM | không |
| Người dùng | ~/.claude/settings.json | Mọi dự án của một người dùng | không |
| Dự án | .claude/settings.json trong thư mục gốc repository | Dự án này, cho cả nhóm | có |
| Local | .claude/settings.local.json | Chỉ dự án này, chỉ người dùng này | không (trong .gitignore) |
Tôi giữ 7 hook mô tả bên dưới trong .claude/settings.json cấp dự án, để chúng tự động được đưa vào khi repository được clone và Claude áp dụng cùng quy tắc trong cửa sổ mới. Các hook thử nghiệm cá nhân được để trong .claude/settings.local.json để không ảnh hưởng đến nhóm.
Cả 4 tệp được hợp nhất khi bắt đầu phiên. Nếu quy tắc xung đột, cấp thấp hơn được ưu tiên (local trên project, project trên user).
Cách nhanh nhất để tạo hook đầu tiên là dùng menu tương tác /hooks ngay trong Claude Code. Chọn một sự kiện trong danh sách, chọn matcher (ví dụ Bash hoặc Edit), rồi dán lệnh vào. Claude Code sẽ ghi JSON hợp lệ vào settings.json cho bạn.
31 sự kiện vòng đời: người mới cần những sự kiện nào?
Điểm chính
Claude Code hiện có 31 sự kiện vòng đời, chia thành 3 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 công cụ (PreToolUse/PostToolUse). Người mới cần 4 trong số 31 sự kiện: PreToolUse cho các hạn chế, PostToolUse cho chỉnh sửa tự động, SessionStart cho ngữ cảnh và Stop cho kiểm tra cuối cùng.
Sự kiện xảy ra theo ba tần suất: một lần mỗi phiên (SessionStart, SessionEnd), một lần mỗi lượt (UserPromptSubmit, Stop, StopFailure) và mỗi lần gọi công cụ trong agentic loop (PreToolUse, PostToolUse).
Cần hiểu ba nhịp này để tránh nhầm lẫn:
- Một lần mỗi phiên -
SessionStart,SessionEnd,Setup. Chúng chạy một lần khi Claude Code mở và đóng. Dùng để khởi tạo ngữ cảnh và dọn tệp tạm. - Một lần mỗi lượt -
UserPromptSubmit,UserPromptExpansion,Stop,StopFailure. Chúng kích hoạt mỗi khi bạn gửi tin nhắn và mỗi khi mô hình dừng. Dùng để ghi lại cuộc trò chuyện và kiểm tra kết quả cuối cùng. - Mỗi lần gọi công cụ -
PreToolUse,PostToolUse,PostToolUseFailure,PermissionRequest,PermissionDenied,PostToolBatch,SubagentStart,SubagentStop. Chúng kích hoạt mỗi lần chỉnh sửa tệp, khởi chạy Bash, đọc hoặc tìm kiếm. Đây là lớp hoạt động dày đặc nhất, nơi đặt các hạn chế, định dạng và bước xác thực.
Danh sách đầy đủ 31 sự kiện còn có các sự kiện bất đồng bộ/phản ứng: TeammateIdle, PreCompact, PostCompact, Elicitation, ElicitationResult, InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, Notification. Chúng dành cho trường hợp nâng cao; hiện tại hãy bỏ qua.
Bài kiểm tra 5 câu hỏi: khi nào quy tắc nên nằm trong hook, khi nào giữ trong CLAUDE.md?
Điểm chính
Nếu một quy tắc nói về hành động (không phải ngữ cảnh), liên quan đến an toàn/tiền/dữ liệu, có thể kiểm tra bằng grep/regex, khó khắc phục nếu vi phạm và Claude đã từng vi phạm ít nhất một lần, thì quy tắc đó nên nằm trong hook. Nếu có ít hơn ba câu trả lời “có” trong năm câu, hãy giữ nó trong CLAUDE.md. Tôi áp dụng bài kiểm tra cho 80 dòng của mình và chuyển 5 quy tắc sang hooks.
Với tôi, cách này giống một đạo luật, không phải lời gợi ý. Tôi rút gọn lựa chọn giữa CLAUDE.md và hook thành bài kiểm tra đơn giản gồm năm câu hỏi. Nếu có ít nhất ba câu trả lời “có”, quy tắc được đưa vào hook. Ít hơn ba thì giữ trong văn bản.
Bài kiểm tra “khi nào quy tắc nên nằm trong hook?”
- Quy tắc này nói về hành động thay vì ngữ cảnh phải không? (“đừng chạy
rm -rf” là hành động; “tôi có dự án TypeScript” là ngữ cảnh.) - Quy tắc này liên quan đến an toàn, tiền bạc hoặc dữ liệu không? (“đừng động vào
.env”, “đừng công bố khóa”.) - Có thể kiểm tra bằng grep, regex hoặc mã thoát của lệnh shell không? (“sau khi sửa
.md, kiểm tra để chắc chắn không có em dash”.) - Nếu vi phạm quy tắc, hậu quả có khó khắc phục không? (“force push lên main” khó khắc phục; “quên dấu chấm cuối câu” thì dễ.)
- Bạn đã từng thấy Claude quên quy tắc này ít nhất một lần chưa?
Ví dụ: “Đừng dùng em dash; chỉ dùng dấu gạch nối.” Hành động? Có. An toàn? Không. Kiểm tra bằng grep? Có. Khó khắc phục? Không. Đã thấy vi phạm? Có. 3 trên 5: đưa vào hook.
Phản ví dụ: “Tôi có vibe coders, không phải developers; hãy viết không dùng tiếng lóng.” Hành động? Nghiêng về ngữ cảnh hơn. An toàn? Không. Có thể kiểm tra bằng regex? Quá khó. Khó khắc phục? Không. Đã thấy vi phạm? Có. 1 trên 5: giữ trong CLAUDE.md.
Năm quy tắc của tôi vượt qua bài kiểm tra. Chúng được chuyển sang hooks. 75% còn lại vẫn nằm trong tệp văn bản.
7 hook sẵn sàng sao chép
Điểm chính
Dưới đây là 7 hook dùng ngay cho .claude/settings.json: chặn chỉnh sửa .env, tự động chạy prettier, ngăn force-push lên main, kiểm tra em dash, giải phóng cổng trước npm run dev, tự động commit sau khi kiểm thử thành công và chèn ngữ cảnh mới khi bắt đầu phiên. Sao chép từng hook vào tệp, khởi động lại Claude Code là dùng được.
Cả 7 hook được lưu trong một tệp .claude/settings.json tại thư mục gốc dự án. Mỗi mục có loại sự kiện, bộ lọc công cụ (matcher) và lệnh shell. Exit 2 từ chối lời gọi PreToolUse khớp trước thực thi. Ở PostToolUse, hành động đã xảy ra; hook báo phản hồi, không hoàn tác.
Với hầu hết sự kiện hook, chỉ exit code 2 mới chặn hành động. Claude Code coi exit code 1 là lỗi không chặn và tiếp tục hành động, dù theo thông lệ Unix, 1 biểu thị thất bại.
exit 0 nghĩa là thành công. exit 1 nghĩa là có lỗi, nhưng Claude vẫn tiếp tục (đây là anti-pattern đầu tiên bên dưới). exit 2 tùy sự kiện: có thể chặn PreToolUse; PostToolUse chỉ phản hồi về hành động đã làm.
Hook 1. Kiểm soát sửa .env trực tiếp (PreToolUse, exit 2)
Chặn việc chỉnh sửa trực tiếp bất kỳ tệp nào khớp với .env, .env.local, .env.production và các tên tương tự.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '(^|/)\\.env(\\.|$)' && { echo 'Do not edit `.env` directly. Tell me which variable to add, and I’ll open the file myself.' >&2; exit 2; } || exit 0"
}
]
}
]
}
}
Cách hoạt động: Trước mỗi Edit/Write/MultiEdit, hook lấy đường dẫn tệp, kiểm tra bằng grep xem có khớp mẫu .env không, và nếu có thì trả exit 2 cùng thông báo. Claude nhìn thấy stderr và nói với tôi: “Tôi đã thử chỉnh sửa .env nhưng hook chặn lại; hãy cho tôi biết cần thêm biến nào.”
Hook 2. Tự động chạy prettier sau mỗi lần sửa mã (PostToolUse, exit 0)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(ts|tsx|js|jsx|json)$' && npx prettier --write \"$FILE\" 2>/dev/null & exit 0"
}
]
}
]
}
}
Cách hoạt động: Sau mỗi Edit/Write, hook lấy đường dẫn và nếu phần mở rộng là .ts/.tsx/.js/.jsx/.json thì chạy prettier trong nền bằng &. Hook luôn thoát với mã 0 vì đây là định dạng, không phải hạn chế. Nếu prettier thất bại cũng không sao; tệp vẫn như cũ.
Khác biệt giữa hook 1 và hook 2 là căn bản: PreToolUse + exit 2 chặn cứng trước hành động. PostToolUse + exit 0 chỉnh sửa mềm sau hành động. Đây là hai chế độ khác nhau cho những nhiệm vụ khác nhau.
Hook 3. Chặn git push --force lên nhánh main (PreToolUse, exit 2)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE 'git[[:space:]]+push' && echo \"$CMD\" | grep -qE '(\\-\\-force|\\-\\-force-with-lease|[[:space:]]-f([[:space:]]|$))' && echo \"$CMD\" | grep -qE '([[:space:]/:])main([[:space:]/:]|$)' && { echo 'Force push to main is forbidden. Create a new branch or ask me to do it manually.' >&2; exit 2; } || exit 0"
}
]
}
]
}
}
Cách hoạt động: Mỗi lần khởi chạy Bash, hook kiểm tra lệnh có chứa push, có --force/--force-with-lease/-f và nhắc đến main hay không. Nếu cả ba điều kiện đều khớp, hook chặn lệnh bằng thông báo rõ ràng.
Trước đây tôi viết quy tắc này bằng chữ in hoa trong CLAUDE.md. Claude vẫn vi phạm khoảng hai tuần một lần khi tôi yêu cầu nó “force push để hoàn tác commit lỗi của tôi”. Hook bắt dạng lệnh tường minh được nêu nhưng alias, script, nhánh đích ngầm hoặc Git client khác có thể vượt kiểm chuỗi. Cưỡng chế bảo vệ main trên remote, giới hạn credential tác nhân có.
Hook 4. Chặn em dash trong tệp văn bản (PostToolUse, exit 2)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(md|mdx|tsx?)$' && grep -q $'\\xe2\\x80\\x94' \"$FILE\" && { echo \"File $FILE contains an em dash (U+2014). Replace it with a regular hyphen (U+002D).\" >&2; exit 2; } || exit 0"
}
]
}
]
}
}
Cách hoạt động: PostToolUse exit 2 báo lỗi hậu kiểm cho Claude sau sửa; không rollback hay ngăn ghi. Nếu Claude sửa tệp mà vẫn để em dash, hook thất bại, Claude nhận phản hồi và thử lại. Sau 2–3 phiên, Claude sẽ thôi chèn ký tự này.
Lưu ý grep tìm chính xác ký tự em dash (U+2014) bằng $'\xe2\x80\x94', cách viết theo cú pháp Bash để biểu diễn nó dưới dạng byte UTF-8. Nếu chỉ viết grep -q '-', nó sẽ bắt cả dấu gạch nối thông thường và chạy trên mọi tệp. Một ký tự vô hình có thể phá hỏng toàn bộ logic.
Hook 5. Giải phóng cổng 3000 trước npm run dev (PreToolUse, exit 0)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE '(npm|pnpm|yarn)[[:space:]]+run[[:space:]]+dev' && { lsof -ti:3000 | xargs -r kill -9 2>/dev/null; echo 'Port 3000 freed before launch' >&2; }; exit 0"
}
]
}
]
}
}
Cách hoạt động: Trước mỗi npm run dev (hoặc pnpm/yarn), hook diệt tiến trình đang dùng cổng 3000. Hook luôn thoát với mã 0 vì đây là bước chuẩn bị, không phải hạn chế. Cách này tránh tình huống Claude thấy “cổng đang được dùng”, tưởng mã có vấn đề rồi bắt đầu sửa, trong khi thực tế máy chủ cũ chỉ còn chạy sót lại.
Hook 6. Tự động commit sau khi kiểm thử thành công (PostToolUse, exit 0)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "INPUT=$(cat); CMD=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty') || exit 1; EXIT=$(printf '%s' \"$INPUT\" | jq -r '.tool_response.exit_code // empty') || exit 1; case \"$CMD\" in 'npm test'|'pnpm test'|'yarn test') ;; *) exit 0 ;; esac; [ \"$EXIT\" = \"0\" ] || exit 0; git diff --cached --quiet || { echo 'Existing staged changes: review manually' >&2; exit 0; }; git add -- src/orders.ts tests/orders.test.ts || exit 1; git diff --cached --quiet && exit 0; git commit -m 'tests: checkpoint after successful run'"
}
]
}
]
}
}
Cách hoạt động: Đọc JSON stdin một lần, chỉ nhận đúng lệnh test thuần đã nêu. Chỉ tiếp khi tool_response.exit_code được cấp rõ là zero; bản cài không có trường đó thì bỏ commit, không đoán thành công. Thay src/orders.ts, tests/orders.test.ts bằng đúng tệp tác vụ sở hữu. Có staged thay đổi trước thì dừng để không gom việc ngoài. Add lỗi dừng, diff rỗng không commit, commit hook thường vẫn bật. Thử input đạt, lỗi và thiếu kết quả ngoại tuyến trước bật.
Phản mẫu: thêm git commit --no-verify vào mọi Edit. Giữ kiểm commit bật. Nếu kiểm kích tự động hóa đệ quy, thu trigger hoặc thiết kế lại kiểm đó; không mặc định bypass xác minh. Xem staged diff và ownership trước bật commit tự động.
Hook 7. Chèn ngữ cảnh mới khi bắt đầu phiên (SessionStart với additionalContext)
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo 'no-branch'); STATUS=$(git status --short 2>/dev/null | head -10); echo \"{\\\"additionalContext\\\":\\\"Current branch: $BRANCH. Uncommitted changes:\\n$STATUS\\\"}\""
}
]
}
]
}
}
Cách hoạt động: Khi bắt đầu phiên, hook thu thập branch Git hiện tại và danh sách các thay đổi chưa commit, rồi trả về JSON có trường additionalContext. Claude Code chèn văn bản này vào system message đầu tiên, để Claude biết ngay từ đầu mình đang ở đâu và còn lại việc gì từ phiên trước.
Dùng additionalContext cho thông tin Claude cần biết về trạng thái môi trường hoặc một thao tác vừa hoàn tất: trạng thái môi trường (branch hiện tại, mục tiêu triển khai, feature flag đang bật), quy tắc dự án có điều kiện (lệnh kiểm thử nào áp dụng cho tệp vừa sửa), dữ liệu bên ngoài (issue đang mở được giao cho bạn, kết quả CI gần đây).
Có thể mở rộng hook này để lấy commit mới nhất và mô tả của nó, trạng thái GitHub Actions, sprint hiện tại trong Linear/Jira và các issue đang mở của repository hiện tại.
3 cạm bẫy hook mà cứ hai người thì một người gặp
Điểm chính
Cạm bẫy đầu tiên: hook với exit 1 vẫn cho qua một cách im lặng, còn Claude bỏ qua lỗi. Thứ hai: công việc nặng trong hook bị dừng sau timeout 60 giây. Thứ ba: hook chỉ thấy các biến môi trường từ ~/.zshrc, không thấy .env của dự án. Mỗi lỗi đều sửa được bằng một dòng, nhưng nếu không biết các cạm bẫy này, bạn có thể nghĩ rằng “hooks không hoạt động”.
Cạm bẫy 1. Hook với exit 1 cho qua một cách im lặng
Đây là lỗi nguy hiểm nhất. Trong một tuần, tôi nghĩ bộ bảo vệ .env hoạt động, nhưng hóa ra trong hook tôi dùng jq -r '.tool_input.file_path' mà không kiểm tra null. Khi tool_input rỗng, jq thoát với mã 1. Claude coi exit 1 là “hook error, tiếp tục”, nên hành động vẫn diễn ra. Tôi không hiểu vì sao .env vẫn bị đụng đến. Chỉ khi tự chạy hook trong terminal với input rỗng, tôi mới thấy nguyên nhân.
Cách sửa chỉ có một quy tắc: bắt đầu mọi hook bằng cách kiểm tra null và kết thúc bằng || exit 0. Khi có lỗi thoát ngoài dự kiến, hook sẽ thoát theo cách “an toàn” thay vì âm thầm cho qua.
# Safe start and end template:
FILE=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
# ... your logic ...
exit 0 # or || exit 0 on one line
Cạm bẫy 2. Hook chậm bị dừng do timeout
Tôi từng có hook chạy eslint --fix trên toàn bộ tệp sau Edit. Tệp lớn (1000+ dòng) mất 8–10 giây để xử lý. Timeout mặc định của hook trong Claude Code là 60 giây nên tôi nghĩ mình vẫn trong giới hạn. Nhưng khi có nhiều thao tác Edit trong một phút, hooks tích tụ và xếp hàng, một số bị bỏ qua. Tôi chỉ nhận ra khi nhật ký hiện “hook timed out”.
Cách sửa: nếu hook cần làm việc nặng, hãy chạy ngầm. Dùng eslint --fix "$FILE" & mà không đợi kết quả. Khi đó hook trả exit 0 trong vài mili giây, còn công việc thật vẫn chạy ở chế độ nền.
# Heavy operation in background:
npx eslint --fix "$FILE" &
exit 0
Cạm bẫy 3. Hook không thể thấy các biến môi trường của dự án
Tôi nghĩ process.env trong hook sẽ khả dụng như trong ứng dụng Node.js. Không phải vậy. Hook chạy trong shell được tiến trình cha Claude Code khởi tạo và chỉ thấy những gì có trong ~/.zshrc hoặc ~/.bashrc, cùng những gì Claude Code truyền vào một cách rõ ràng. Nếu hook cần OPENAI_API_KEY từ .env, bạn phải tự đọc .env.
Cách sửa: đọc rõ ràng .env trong lệnh hook.
# Passing project env into hook:
cd "$PWD" && source .env && curl -H "Authorization: Bearer $OPENAI_API_KEY" ...
# Or with dotenv:
dotenv -e .env -- node my-validator.js
Điều này đặc biệt phiền trong monorepo, nơi mỗi package có tệp .env riêng. Khắc phục bằng cách truyền PWD và chạy rõ ràng cd "$PWD" ở đầu lệnh.
Kiểm thử hooks trước khi commit như thế nào?
Điểm chính
Hook là một lệnh shell. Bạn có thể tự chạy nó trong terminal với stdin giả, kiểm tra mã thoát và stderr trước khi Claude Code chạy hook. Cách này tiết kiệm nhiều giờ gỡ lỗi và bảo vệ bạn khỏi những hook tự chặn hoạt động của chính nó.
Kịch bản kiểm thử cơ bản. Lấy lệnh hook từ JSON, mở terminal và truyền JSON mà Claude Code sẽ gửi:
# Test hook 1 (protect .env):
echo '{"tool_input":{"file_path":".env"}}' | bash -c '
FILE=$(jq -r ".tool_input.file_path // empty")
[ -z "$FILE" ] && exit 0
echo "$FILE" | grep -qE "(^|/)\.env(\.|$)" && {
echo "BLOCKED" >&2
exit 2
} || exit 0
'
echo "Exit code: $?"
Bạn sẽ thấy BLOCKED trong stderr và Exit code: 2. Nếu nhận được exit 0, regex không hoạt động; hãy sửa grep.
Các tình huống cần kiểm tra:
- Trường hợp thành công - tệp cần bị chặn.
.envcho hook 1,git push --force maincho hook 3. - Trường hợp âm - tệp không bị chặn.
README.mdcho hook 1,git push origin feature-branchcho hook 3. - Trường hợp biên - input rỗng, giá trị null, ký tự bất ngờ. Hook phải trả exit 0 hoặc thất bại với lỗi rõ ràng, không được âm thầm cho qua.
Nơi đặt hooks thử nghiệm. Không đặt trong .claude/settings.json, mà trong .claude/settings.local.json (không nằm trong git). Thêm hooks mới ở đó dưới PostToolUse, dùng echo thay cho hành động thật, kiểm tra xem chúng có kích hoạt không rồi chuyển sang settings.json dùng trong production.
// Experiment in settings.local.json - writes to /tmp/hook-debug.log:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "echo \"[hook fired] $(date) $(jq -r '.tool_input.file_path // \"?\"')\" >> /tmp/hook-debug.log"
}
]
}
]
}
}
Sau 5–10 lần chỉnh sửa trong Claude Code, mở /tmp/hook-debug.log để xem những tệp nào đã kích hoạt hook. Nếu tệp trống, matcher không phù hợp; hãy sửa regex.
Tiếp theo là gì?
Điểm chính
Bắt đầu với hooks PostToolUse có exit 0; chúng an toàn nhất và không chặn gì cả. Sau một tuần, thêm hook PreToolUse đầu tiên với exit 2 để bảo vệ .env hoặc ngăn force-push. Sau hai tuần, CLAUDE.md sẽ rút từ bản thỏa thuận 200 dòng thành mô tả ngữ cảnh ngắn 50–80 dòng, còn kiểm sự kiện chọn chuyển sang hook, giới hạn quyền cứng được cưỡng chế riêng bằng permissions, sandbox và bảo vệ remote.
5 bước cho tuần tới:
- Grep
CLAUDE.mdđể tìm những từ như “never,” “forbidden,” “always” và “must”. Mỗi quy tắc như vậy là ứng viên cho hook. Nếu một quy tắc mang tính mệnh lệnh, tệp văn bản sẽ không thực thi nó. - Bắt đầu với PostToolUse + exit 0. Đây là cách an toàn nhất: chúng thay đổi điều gì đó nhưng không chặn hành động nào. Prettier (hook 2), dừng máy chủ dev (hook 5), tự động commit sau khi kiểm thử (hook 6).
- Chỉ khi tự tin mới thêm PreToolUse + exit 2 đầu tiên. Ví dụ, bảo vệ
.env(hook 1) hoặc ngăn force-push (hook 3). Đây là lệnh chặn cứng; một lỗi trong lệnh grep có thể chặn chính bạn. - Tự kiểm thử hooks trước khi commit (xem phần bên trên). Cách này tiết kiệm nhiều giờ gỡ lỗi.
- Dùng
settings.local.jsoncho hooks thử nghiệm. Thêm hooks mới vào đó và dùngechothay vì hành động thật.
Sau khi cấu hình hooks, bước tiếp theo trong cụm hướng dẫn là cách hoàn tác thay đổi trong Claude Code, phòng trường hợp hook để lọt điều gì nguy hiểm. Nếu muốn Claude nhớ cấu trúc dự án giữa các phiên, hãy xây dựng Second Brain trong Claude Code (thư mục business/ + kho kiến thức được liên kết). Và để đặt tên cho các skill của dự án để tái sử dụng, hãy xem Claude Code Skills.
Nguồn
Nếu cần truy cập các mạng nơ-ron qua Ailoot, hãy xem Ailoot.
Xem thêm chi tiết
Câu hỏi thường gặp
Vì sao hook phải đọc JSON stdin chỉ một lần?
Stdin là luồng: jq đầu tiêu thụ, lần sau gặp EOF. Lưu đầu vào một lần rồi lấy trường lệnh và kết quả từ giá trị đó. Kết quả thiếu hoặc lạ coi là chưa xác minh và bỏ auto-commit.
Hook regex ví dụ có tạo ranh giới bảo mật không?
Không. Chúng kiểm sự kiện và chuỗi nhận diện được. Shell redirection, alias, script, MCP hay đường ghi khác có thể vượt; dùng credential hạn chế, quyền tệp, sandbox và nhánh bảo vệ cho giới hạn cứng.