git pre-commitフックで.envの誤コミットを防ぐ

GitHubにコードを上げ始めた頃、僕が一番怖かったのは「.envをうっかりコミットすること」だった。APIキーが入ったファイルを一度でもpushしたら、履歴に残る。
消したつもりでも残る。あと、lint(コードの書き方の誤りを自動で見つけるチェック)をかけ忘れたまま上げて、後から崩れたコードに気づくのも地味にへこむ。
で、その両方をコミットの瞬間に機械に止めさせる仕組みが、gitに最初から入っている。それが「pre-commitフック」だ。
目次 / INDEX
pre-commitフックとは。コミット直前に走る門番
結論から言うと、pre-commitフックは「コミットが作られる直前に自動で走るスクリプト」だ。仕組みはすごく単純で、覚えることは1個しかない。
| スクリプトの終了コード | コミットは |
|---|---|
| 0 | 普通に作られる |
| 0以外 | 中断される |
これだけ。スクリプトが「ダメだ」と非ゼロで終われば、コミット自体が成立しない。この「非ゼロで止める」がフックの肝で、逆に言えば、チェックしたい内容をシェルスクリプトで書けるなら何でも門番にできる。
置き場所は各リポジトリの .git/hooks/ ディレクトリ。ここを覗くと、最初から pre-commit.sample みたいなサンプルが .sample 付きで入っている。.sample が付いたままでは動かず、拡張子を外した pre-commit という名前の実行可能ファイルを置くと有効になる。
ツールのインストールすら要らない。gitを入れた時点で土台は揃っているんだ。
なお、.gitignoreに .env を書いておくのが第一防衛線で、pre-commitフックはそれを二重にする保険という位置づけだと思う。ignoreし忘れた新しい秘密ファイル、リネームした瞬間の取りこぼし、そういう「ignoreの網から漏れた1回」を最後に拾うのがフックの役目。第一防衛線の方は .gitignoreの書き方これだけ にまとめてある。
最小のpre-commitフックを作る手順(.env検知)
じゃあ作る。手順は3ステップで、5分あれば終わる。
.git/hooks/pre-commitというファイルを作る(拡張子なし)- 下のスクリプトを貼る
- 実行権限を付ける
中身はこれだけでいい。
#!/bin/sh
# これからコミットされるファイル一覧に .env が混ざっていたら止める
if git diff --cached --name-only | grep -e "\.env$" -e "\.key$"
then
echo "秘密情報っぽいファイルがステージされている。コミットを中断した"
exit 1
fi
exit 0
ポイントは git diff --cached --name-only。これは「ステージ済み、つまり今からコミットされるファイルの名前一覧」を出すコマンドだ。
作業ツリーに.envが転がっているだけならセーフで、git add されてコミットに入ろうとした瞬間だけ引っかかる。誤検知が少ないのはこの設計のおかげだと思う。
貼ったら実行権限を付ける。
chmod +x .git/hooks/pre-commit
ここ、僕が一番強調したいつまずきポイントで、権限を付け忘れるとフックは「エラーも出さずにただ無視される」。ちゃんと動いていると思い込んだまま素通りするのが一番怖いので、作ったら必ずテストする。
touch .env
git add -f .env
git commit -m "test"
# → メッセージが出てコミットが中断されれば成功
止まるのを一回この目で見ておくと、以後の安心感が全然違う。「たぶん守られてるはず」と「止まるのを見た」の差は大きい!
あと、Windowsの人へ。フックのスクリプトはsh経由で走るので、Git Bashが入っていれば同じように動くはずだ。ただ細かい挙動は環境差があるので、そこは自分の環境で上のテストを一回回して確かめてほしい。
lint忘れも同じ仕組みで防げる
秘密情報の検知ができたら、次はlintだ。考え方は完全に同じで、「lintコマンドが失敗したら非ゼロで終わる」ようにするだけ。多くのlintツールは失敗時に非ゼロを返すので、素直に書ける。
#!/bin/sh
# 例: npmプロジェクトでlintを回し、失敗したらコミットを止める
npm run lint
if [ $? -ne 0 ]
then
echo "lintが通らなかったのでコミットを中断した"
exit 1
fi
exit 0
pre-commitフックの定番の使いどころを並べると、だいたいこの3つに落ち着く。
- 秘密情報の混入検知(.envや
*.keyがステージされていないか) - lint/formatの自動実行(失敗したらコミット不成立)
- デバッグ用のprintやconsole.logの消し忘れ検知
対比するとわかりやすいんだけど、lintを「手で毎回かける」運用は自分の記憶力が頼りで、疲れてる深夜ほど抜ける。フックに入れる運用は、コミットという「絶対に通る関門」に検査を縛り付けるので、疲れていても抜けない。人間の注意力をあてにしない側に倒すのが正解だと思う。
落とし穴が2つ。共有されない問題と—no-verify
ここまでで日常は回るんだけど、知らないとハマる仕様が2つある。
1つ目。.git/hooks/ はgitの管理対象じゃない。リポジトリに含まれないから、cloneしても他人には配られないし、自分の別PCにも付いてこない。
完全にローカル専用だ。もし共有したければ、リポジトリに追跡させる別ディレクトリ(例えば .githooks/)にスクリプトを置いて、
git config core.hooksPath .githooks
でフックの参照先をそこに向ける方法がある。個人リポジトリのうちは .git/hooks/ 直置きで十分で、複数台やチームで揃えたくなったら移行すればいい。この core.hooksPath がなぜcloneで配られないのかは、git configの3層のlocal層の話につながる。
2つ目。git commit --no-verify(短縮形 -n)を付けるとフックは丸ごとスキップされる。フックが壊れて何もコミットできない、みたいな緊急時に通せる脱出口ではあるんだけど、これは自分で張った安全網を自分で素通りする行為でもある。
「急いでるから-nで」を癖にした瞬間、フックを書いた意味が消える。両刃だと思って、使うのは本当に例外のときだけにしたい。
紛らわしい話をもう1つだけ。同じ「pre-commit」という名前で、YAMLで設定を書く専門のフレームワーク(pre-commit.com)も存在する。検索するとこっちの記事もよく出てくるけど、別物だ。
まずは素の .git/hooks/pre-commit を1枚書くのが入門で、複数リポジトリでチェックを揃えたくなった段階でそういうツールを検討すればいいはず。最初からYAMLに行く必要はない。
まとめ
pre-commitフックは、コミット直前に走るシェルスクリプト1枚で、非ゼロで終わればコミットが止まる。それだけの仕組みだけど、「.envを上げる事故」と「lint忘れ」という学生の個人開発で一番ありがちな2つのミスを、自分の注意力に頼らず機械に防がせられる。
まずは今開いているリポジトリで、この記事の.env検知フックをコピペして chmod +x して、わざとテストコミットを止めてみてほしい。門番が働くのを一度見れば、あとは安心してコードに集中できる。
コミットやステージといった言葉がまだあやふやなら、git/GitHub入門で基本の流れを一周してから戻ってくると、このフックが何を見張っているのかがはっきりする。
よくある質問
pre-commitフックとは何?
コミットが作られる直前に自動で走るスクリプト。各リポジトリの .git/hooks/pre-commit に実行可能ファイルとして置く。スクリプトが0以外の終了コードで終わるとコミットが中断される。gitを入れた時点で使えるので追加インストールは要らない。
pre-commitフックを置いたのに動かないのはなぜ?
実行権限を付け忘れている可能性が高い。chmod +x .git/hooks/pre-commit を付けないと、フックはエラーも出さずに無視される。作ったら .env をわざとステージしてテストコミットし、止まるのを一度確認する。
pre-commitフックはcloneした相手にも配られる?
配られない。.git/hooks/ はgitの管理対象外で、完全にローカル専用。共有したければ .githooks/ のような追跡されるディレクトリにスクリプトを置き、git config core.hooksPath .githooks で参照先を向ける。