Chuyện xảy ra khi không có Git hooks
Mình từng làm trong team 8 người, và cứ vài ngày lại có ai đó push code lên rồi CI pipeline đỏ lè — hoặc vì quên chạy linter, hoặc commit message kiểu fix, test, aaaaa. Mỗi sprint khoảng 3-4 lần như vậy. Review code mà phải comment “bạn ơi format lại đi” thì vừa mất thời gian, vừa tạo friction không đáng có.
Git hooks giải quyết đúng cái đó — tự động chặn những thứ sai trước khi chúng vào repo, không cần ai nhắc.
Git hooks là gì, nằm ở đâu?
Git hooks thực chất là script thông thường — bash, Python, Node.js, gì cũng được — mà Git gọi tự động vào những thời điểm nhất định. Trước khi commit, trước khi push, sau khi merge… mỗi sự kiện đều có hook tương ứng.
Mỗi Git repo đều có sẵn thư mục .git/hooks/, trong đó chứa các file mẫu .sample. Để kích hoạt hook, tạo file đúng tên (bỏ đuôi .sample) và cấp quyền thực thi:
ls .git/hooks/
# applypatch-msg.sample commit-msg.sample pre-commit.sample pre-push.sample ...
# Tạo hook mới
touch .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
So sánh client-side vs server-side hooks
Hai nhóm này hoạt động ở tầng hoàn toàn khác nhau — nhầm chỗ thì setup xong vẫn không có tác dụng gì.
Client-side hooks — chạy trên máy dev
Chạy trên máy của từng developer, trước hoặc sau các thao tác local:
- pre-commit: chạy trước khi tạo commit — dùng để lint, format, chạy unit test nhanh
- commit-msg: chạy sau khi nhập commit message — dùng để validate format (Conventional Commits, Jira ticket ID, v.v.)
- pre-push: chạy trước khi push — dùng để chạy test suite đầy đủ hơn
- post-commit: chạy sau khi commit thành công — thường dùng để notify hoặc log
Nhược điểm cần biết trước: .git/hooks/ không được Git track, nên mỗi developer phải tự cài sau khi clone. Nếu không có script setup, người mới vào team sẽ bỏ qua bước này mà không hay.
Server-side hooks — chạy trên remote repo
Chạy trên server (GitHub Actions không phải là hook, nhưng GitLab/Gitea self-hosted hỗ trợ):
- pre-receive: chạy trước khi server nhận push — có thể reject toàn bộ push
- post-receive: chạy sau khi push thành công — thường dùng để deploy, notify
- update: tương tự pre-receive nhưng chạy per-branch
Server-side hooks không ai bypass được — developer không dùng --no-verify để qua được. Đổi lại, cần quyền truy cập server. GitHub/GitLab.com không cho cài server hooks, nên phải dùng CI/CD thay thế.
Manual hooks vs Husky — Cái nào phù hợp hơn?
Câu hỏi mình hay nhận nhất từ junior dev khi setup lần đầu: “dùng cái nào?” Câu trả lời thực ra phụ thuộc vào tech stack hơn là sở thích cá nhân.
Manual hooks: đơn giản, không phụ thuộc
Viết thẳng script vào .git/hooks/, hoặc tạo thư mục hooks/ trong repo rồi có script setup.sh để symlink:
# hooks/setup.sh
#!/bin/bash
cp hooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
echo "Git hooks installed!"
Ưu điểm: không cần Node.js, không phụ thuộc thêm package nào, chạy được với mọi ngôn ngữ (Python, Bash, Ruby…).
Nhược điểm: phải nhớ chạy setup.sh sau khi clone, dễ bị quên.
Husky: tự động cài khi npm install
Husky là npm package, tích hợp hooks vào package.json lifecycle. Khi dev chạy npm install, hooks được cài tự động — không cần thêm bước nào.
npm install --save-dev husky
npx husky init
Ưu điểm: zero-config cho team, hooks được commit vào repo (.husky/), không ai bị thiếu.
Nhược điểm: cần Node.js và npm, thêm dependency vào project non-JS thì hơi overkill.
Kinh nghiệm của mình: project JavaScript/TypeScript thì dùng Husky, còn Python/Go/đa ngôn ngữ thì dùng manual hooks với script setup đặt trong Makefile hoặc README.
Triển khai thực tế: 3 hooks quan trọng nhất
1. pre-commit: chặn code lỗi trước khi commit
#!/bin/bash
# .git/hooks/pre-commit
echo "Running pre-commit checks..."
# Lấy danh sách file Python đang staged
STAGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
if [ -n "$STAGED_PY" ]; then
# Chạy flake8 lint
echo "Linting Python files..."
flake8 $STAGED_PY
if [ $? -ne 0 ]; then
echo "Linting failed. Fix errors before committing."
exit 1
fi
# Chạy black format check
black --check $STAGED_PY
if [ $? -ne 0 ]; then
echo "Format check failed. Run 'black .' to fix."
exit 1
fi
fi
echo "All checks passed!"
exit 0
Hook này chỉ kiểm tra file đang staged, không phải toàn bộ repo — nên thường xong trong 1-2 giây, không ảnh hưởng nhiều đến flow commit.
2. commit-msg: enforce format commit message
Conventional Commits (feat:, fix:, docs:…) giúp auto-generate changelog và dễ đọc history. Hook này validate pattern:
#!/bin/bash
# .git/hooks/commit-msg
MSG_FILE=$1
MSG=$(cat $MSG_FILE)
# Pattern: type(scope): description
# Ví dụ: feat(auth): add OAuth2 login
PATTERN='^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,72}$'
if ! echo "$MSG" | grep -qE "$PATTERN"; then
echo "ERROR: Commit message không đúng format!"
echo "Expected: feat|fix|docs|style|refactor|test|chore(scope): description"
echo "Example: feat(auth): add OAuth2 login"
echo ""
echo "Your message: $MSG"
exit 1
fi
exit 0
3. pre-push: chạy test trước khi push
#!/bin/bash
# .git/hooks/pre-push
BRANCH=$(git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,')
# Chỉ chạy full test khi push lên main hoặc develop
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "develop" ]; then
echo "Running test suite before push to $BRANCH..."
python -m pytest tests/ -q --tb=short
if [ $? -ne 0 ]; then
echo "Tests failed. Push aborted."
exit 1
fi
fi
exit 0
Setup với Husky + lint-staged (cho project JS)
npm install --save-dev husky lint-staged
npx husky init
Thêm vào package.json:
{
"lint-staged": {
"*.{js,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{css,md}": "prettier --write"
},
"scripts": {
"prepare": "husky"
}
}
File .husky/pre-commit:
#!/bin/sh
npx lint-staged
Tips để hooks không trở thành gánh nặng
- Chỉ check file staged, không check toàn repo — dùng
git diff --cached --name-only. Hook chạy quá 5 giây thì dev sẽ tìm cách bypass. - Để lối thoát khi cần gấp:
git commit --no-verify -m "hotfix: emergency". Đôi khi cần push nhanh rồi fix sau — khóa cứng 100% thường phản tác dụng hơn là giúp ích. - Thông báo rõ ràng khi fail: đừng chỉ
exit 1, hãy in ra lỗi cụ thể và câu lệnh để fix. - Commit
hooks/vào repo cùng scriptinstall-hooks.sh— để onboarding thành viên mới không phải hỏi thêm bước nào.
Sau khi mình áp dụng pre-commit lint và commit-msg validation cho team đó, số lượng comment “fix format/lint” trong PR giảm từ khoảng 4-5 comments/PR xuống gần bằng 0. Review tập trung hẳn vào logic, architecture. Merge conflict cũng ít hơn vì code đã được format nhất quán từ trước khi merge.
Git hooks không thay thế được CI/CD — integration test hay end-to-end vẫn cần chạy trên pipeline. Nhưng với những check đơn giản chạy xong trong vài giây, đặt ở client-side hook tiết kiệm thời gian hơn nhiều so với đợi pipeline 5-10 phút rồi mới biết lỗi.

