Git hooksがない場合に起こること
以前8人チームで働いていたとき、数日おきに誰かがコードをpushしてCIパイプラインが真っ赤になる、ということが繰り返されていた。linterを実行し忘れたり、fix、test、aaaaaのようなcommit messageだったりが原因だ。スプリントごとに3〜4回はそういうことがあった。コードレビューで「フォーマットを直してください」とコメントするのは時間の無駄だし、不必要なフリクションを生むだけだ。
Git hooksはまさにその問題を解決する。誰かが指摘しなくても、間違いがリポジトリに入る前に自動的にブロックしてくれる。
Git hooksとは何か、どこにあるのか
Git hooksは通常のスクリプト(bash、Python、Node.jsなど何でも可)で、Gitが特定のタイミングで自動的に呼び出す仕組みだ。commitの前、pushの前、mergeの後など、各イベントに対応するhookが存在する。
すべてのGitリポジトリには.git/hooks/ディレクトリがあり、.sampleというサンプルファイルが含まれている。hookを有効にするには、正しいファイル名(.sample拡張子を除く)でファイルを作成し、実行権限を付与する:
ls .git/hooks/
# applypatch-msg.sample commit-msg.sample pre-commit.sample pre-push.sample ...
# 新しいhookを作成
touch .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
client-sideとserver-side hooksの比較
この2つのグループはまったく異なるレイヤーで動作する。混同すると、設定しても何も効果がない。
Client-side hooks — 開発者のマシンで実行
各開発者のマシンで、ローカル操作の前後に実行される:
- pre-commit:commitを作成する前に実行 — lint、format、高速なunit testに使用
- commit-msg:commit messageを入力した後に実行 — フォーマットの検証(Conventional Commits、Jira ticket IDなど)に使用
- pre-push:pushの前に実行 — より完全なtest suiteの実行に使用
- post-commit:commit成功後に実行 — 通知やログに使われることが多い
事前に知っておくべき欠点:.git/hooks/はGitで追跡されないため、各開発者はclone後に自分でインストールする必要がある。setupスクリプトがなければ、チームの新メンバーがこの手順を気づかずにスキップしてしまう可能性がある。
Server-side hooks — リモートリポジトリで実行
サーバー上で実行される(GitHub ActionsはhookではないがGitLab/Gitea self-hostedはサポートしている):
- pre-receive:サーバーがpushを受け取る前に実行 — push全体を拒否できる
- post-receive:push成功後に実行 — deployや通知によく使われる
- update:pre-receiveに似ているが、ブランチごとに実行される
Server-side hooksは誰もbypassできない。開発者が--no-verifyを使っても回避できない。その代わり、サーバーへのアクセス権限が必要だ。GitHub/GitLab.comはserver hooksのインストールを許可していないため、代わりにCI/CDを使う必要がある。
Manual hooksとHusky — どちらが適しているか?
初めてセットアップするjunior devからよく受ける質問が「どっちを使えばいい?」だ。答えは個人の好みよりもtech stackに依存することが多い。
Manual hooks:シンプルで依存関係なし
スクリプトを直接.git/hooks/に書くか、リポジトリにhooks/ディレクトリを作成してsymlinkするためのsetup.shスクリプトを用意する:
# hooks/setup.sh
#!/bin/bash
cp hooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
echo "Git hooks installed!"
メリット:Node.jsが不要で、追加パッケージへの依存もなく、あらゆる言語(Python、Bash、Rubyなど)で動作する。
デメリット:clone後にsetup.shを実行することを覚えておく必要があり、忘れやすい。
Husky:npm install時に自動インストール
Huskyはnpmパッケージで、hooksをpackage.jsonのlifecycleに統合する。開発者がnpm installを実行すると、hooksが自動的にインストールされる。追加の手順は不要だ。
npm install --save-dev husky
npx husky init
メリット:チームにとってzero-config、hooksがリポジトリ(.husky/)にcommitされるため誰も欠かさない。
デメリット:Node.jsとnpmが必要で、non-JSプロジェクトへの依存追加はやや過剰かもしれない。
私の経験では、JavaScript/TypeScriptプロジェクトにはHuskyを使い、Python/Go/多言語プロジェクトにはMakefileまたはREADMEにsetupスクリプトを置いたmanual hooksを使う。
実践的な実装:最も重要な3つのhooks
1. pre-commit:commitの前にバグのあるコードをブロック
#!/bin/bash
# .git/hooks/pre-commit
echo "Running pre-commit checks..."
# stagingされているPythonファイルの一覧を取得
STAGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
if [ -n "$STAGED_PY" ]; then
# flake8 lintを実行
echo "Linting Python files..."
flake8 $STAGED_PY
if [ $? -ne 0 ]; then
echo "Linting failed. Fix errors before committing."
exit 1
fi
# blackのフォーマットチェックを実行
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はリポジトリ全体ではなく、stagingされているファイルのみをチェックするため、通常1〜2秒で完了し、commitのフローへの影響は最小限だ。
2. commit-msg:commit messageのフォーマットを強制
Conventional Commits(feat:、fix:、docs:など)はchangelogの自動生成と履歴の読みやすさを向上させる。このhookはパターンを検証する:
#!/bin/bash
# .git/hooks/commit-msg
MSG_FILE=$1
MSG=$(cat $MSG_FILE)
# Pattern: type(scope): description
# 例: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のフォーマットが正しくありません!"
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:pushの前にテストを実行
#!/bin/bash
# .git/hooks/pre-push
BRANCH=$(git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,')
# mainまたはdevelopへのpush時のみfull testを実行
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
Husky + lint-stagedのセットアップ(JSプロジェクト向け)
npm install --save-dev husky lint-staged
npx husky init
package.jsonに追加:
{
"lint-staged": {
"*.{js,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{css,md}": "prettier --write"
},
"scripts": {
"prepare": "husky"
}
}
.husky/pre-commitファイル:
#!/bin/sh
npx lint-staged
hooksが負担にならないためのヒント
- stagingされたファイルのみチェックし、リポジトリ全体はチェックしない —
git diff --cached --name-onlyを使用。hookが5秒以上かかると開発者はbypassしようとする。 - 緊急時の逃げ道を用意する:
git commit --no-verify -m "hotfix: emergency"。急いでpushして後から修正する必要がある場合もある。完全にロックすると、助けになるよりも逆効果になることが多い。 - 失敗時は明確なメッセージを表示する:
exit 1だけでなく、具体的なエラーと修正するためのコマンドを出力する。 hooks/をリポジトリにcommitし、install-hooks.shスクリプトと一緒に管理する。新メンバーのオンボーディング時に追加の手順を聞かずに済む。
pre-commit lintとcommit-msg validationをそのチームに適用した後、PRの「format/lintを修正してください」コメントが1PR当たり4〜5件からほぼゼロに減少した。レビューがロジックやアーキテクチャに集中できるようになった。mergeの前にコードが統一されたフォーマットになっているため、merge conflictも減った。
Git hooksはCI/CDの代わりにはならない。integration testやend-to-endテストは引き続きpipelineで実行する必要がある。しかし、数秒で完了するシンプルなチェックであれば、client-side hookに置いておく方が、5〜10分待ってからエラーを知るよりもはるかに時間の節約になる。

