Git có một cơ chế tự động hóa ít người khai thác đúng mức: hooks — các script Git tự gọi ở những thời điểm nhất định (trước khi commit, sau khi commit, trước khi push...). Chúng cho phép ép quy tắc mà không cần nhớ: chặn commit còn console.log/TODO, bắt buộc message theo định dạng, chạy linter/test trước khi push. Nhưng hooks cũng có một cái bẫy lớn khiến nhiều đội tưởng đã "bật" mà thực ra không. Đo thật bằng git 2.39.

Hook là script chạy ở sự kiện Git

Hooks nằm trong thư mục .git/hooks/, tên file = tên sự kiện, cần quyền thực thi (chmod +x). Git cài sẵn các file mẫu *.sample — đổi tên bỏ .sample là kích hoạt.

  • pre-commit: chạy trước khi commit. Exit code khác 0 → hủy commit. Dùng để chặn code xấu.
  • commit-msg: nhận đường dẫn file chứa message → kiểm định dạng, chặn nếu sai.
  • pre-push: chạy trước push → chặn nếu test fail.
# .git/hooks/pre-commit (đã chmod +x)
#!/bin/bash
git diff --cached | grep -q "TODO_KHONG_COMMIT" && { echo "còn TODO!"; exit 1; }
exit 0

Ảnh chụp đoạn mã nền tối giải thích git hooks chạy script tự động ở các sự kiện Git, hook là script Git tự gọi ở một thời điểm kịch bản đặt ở .git hooks tên bằng tên sự kiện chmod +x để bật Git có sẵn file mẫu sample đổi tên bỏ .sample là dùng pre-commit chạy trước khi commit exit khác 0 thì hủy commit commit-msg nhận file message kiểm định dạng chặn nếu sai pre-push chạy trước push chặn push nếu test fail, client hooks vs server hooks client pre-commit commit-msg pre-push chạy trên máy dev tiện nhưng dev có thể bỏ qua --no-verify server pre-receive update post-receive chạy trên máy chủ không bỏ qua được nơi ép luật thật sự CI CD chặn force, bẫy lớn hook không tự chia sẻ theo repo .git hooks nằm trong .git không được commit không clone theo đồng đội clone về không có hook cách chia sẻ đặt hook trong repo .githooks rồi git config core.hooksPath .githooks hoặc husky pre-commit

Hình 1: Hook là script ở .git/hooks/, tên = tên sự kiện, exit != 0 thì hủy hành động. Client hook (dev bỏ qua được bằng --no-verify) vs server hook (ép luật thật sự). Bẫy lớn: hook nằm trong .git nên không clone theo.

Đo thật: chặn commit theo luật

Ảnh chụp bảng kết quả đo thật nền tối chạy git 2.39, phần một hook nằm ở .git hooks có mẫu sample pre-commit.sample commit-msg.sample pre-push.sample đổi tên bỏ .sample cộng chmod +x để kích hoạt, phần hai pre-commit chặn commit có TODO code có TODO_KHONG_COMMIT git commit pre-commit Tu choi con TODO_KHONG_COMMIT commit bị chặn hook exit khác 0 sửa bỏ TODO commit thành công 3f0152a, phần ba commit-msg bắt buộc mã ticket ABC-123 msg sua linh tinh bị chặn thiếu ticket msg APP-42 sua dung chuan thành công, phần bốn nhớ hook cục bộ không clone theo .git hooks không được commit đồng đội không có git config core.hooksPath .githooks để chia sẻ qua repo

Hình 2: .git/hooks có các mẫu .sample. pre-commit chặn commit chứa TODO_KHONG_COMMIT (exit != 0 → hủy), sửa bỏ thì commit thành công (3f0152a). commit-msg bắt buộc mã ticket: "sua linh tinh" bị chặn, "[APP-42] ..." qua. Nhớ dùng core.hooksPath để chia sẻ.

Kết quả cho thấy hook hoạt động đúng:

  • pre-commit chặn theo nội dung: commit code chứa TODO_KHONG_COMMIT → hook in cảnh báo và trả exit khác 0 → Git hủy commit. Sửa bỏ dòng đó, commit thành công (3f0152a). Đây là cách chặn console.log, secret, debugger, file lớn... lọt vào commit.
  • commit-msg ép định dạng: hook kiểm message qua regex [A-Z]+-[0-9]+; message "sua linh tinh" (thiếu ticket) → bị chặn, "[APP-42] sua dung chuan" → qua. Rất hợp để bắt buộc quy ước commit (Conventional Commits, mã ticket) trong đội.

Client hooks vs server hooks — và ai ép được luật

Đây là phân biệt cốt lõi:

  • Client hooks (pre-commit, commit-msg, pre-push) chạy trên máy dev. Tiện, phản hồi nhanh — nhưng dev bỏ qua được bằng git commit --no-verify. Nên chúng là trợ giúp, không phải hàng rào cứng.
  • Server hooks (pre-receive, update, post-receive) chạy trên máy chủ khi nhận push. Dev không bỏ qua được — đây mới là nơi ép luật thật sự (từ chối push không đúng chuẩn, chặn force-push vào nhánh chính, kích hoạt CI/CD). Trên GitHub/GitLab, vai trò này thường do "branch protection rules" và CI đảm nhận thay cho server hook thủ công.

Cái bẫy lớn: hook không clone theo

.git/hooks/ nằm trong thư mục .git — không được commit, không clone theo. Nghĩa là bạn viết pre-commit tuyệt vời trên máy mình, nhưng đồng đội clone repo về không có nó. Cả đội tưởng đã bật kiểm tra nhưng thực ra chỉ máy bạn có. Cách chia sẻ hook qua repo:

  • Đặt hook trong một thư mục được commit (ví dụ .githooks/), rồi mỗi người chạy git config core.hooksPath .githooks (hoặc script setup làm giúp).
  • Hoặc dùng công cụ quản hook: husky (JS), pre-commit (Python, đa ngôn ngữ), lefthook. Chúng cài hook từ file cấu hình trong repo, đồng bộ cho cả đội.

Đánh đổi và lưu ý

Hook phải nhanh. pre-commit chạy mỗi lần commit; nếu nó lint cả repo mất 30 giây, dev sẽ --no-verify cho nhanh — phản tác dụng. Chỉ kiểm file đang staged (git diff --cached --name-only), không quét toàn bộ. Việc nặng (test đầy đủ) để cho CI.

Đừng để hook chặn ẩu. Hook chặn sai (false positive) làm dev bực và học cách bỏ qua. Quy tắc phải rõ ràng, thông báo lỗi phải nói chính xác sai gì và sửa thế nào. Và luôn có đường thoát hợp lý cho trường hợp khẩn (nhưng đừng lạm dụng --no-verify).

Hook không thay CI. Client hook là "lưới đầu tiên" trên máy dev, nhanh và tiện; nhưng vì bỏ qua được và không chia sẻ chắc chắn, đừng coi nó là bảo đảm cuối cùng. Luật thật sự (test phải xanh, chuẩn code) phải ép ở CI/server, nơi không ai vượt được.

Ba ý mang về

  1. Hook là script Git tự chạy ở sự kiện, exit != 0 thì hủy hành động: đo thật, pre-commit chặn commit chứa TODO_KHONG_COMMIT, commit-msg bắt buộc mã ticket [APP-42] — tự động ép quy tắc mà không cần nhớ.
  2. Client hook bỏ qua được (--no-verify), server hook mới ép luật thật: client là trợ giúp nhanh trên máy dev; luật cứng (chặn push sai, force-push) phải ở server/CI/branch protection.
  3. Hook không clone theo (nằm trong .git) — đồng đội không có; chia sẻ bằng core.hooksPath trỏ vào thư mục được commit, hoặc dùng husky/pre-commit; giữ hook nhanh và đừng coi nó thay CI.

Nguồn

Phần sau ta dùng rebase ở chế độ mạnh nhất: git rebase -i (tương tác) — squash gộp commit, reword sửa message, reorder đổi thứ tự, dọn lịch sử nhánh cho sạch trước khi mở PR.