「pushの前にテストを必ず回したい。でもフックの共有のためだけにhuskyを入れるのは大げさな気がする」——そんな場面、意外と多いのではないでしょうか。筆者も実際のプロジェクトで、依存パッケージを1つも増やさずにpre-pushフックをチーム全員へ配布する必要があり、Git標準の core.hooksPath と package.json の prepare スクリプトだけで実現しました。この記事では、そのままコピペで使える最小構成と、導入直後に隠れていた型エラー(うち1件は実バグ)が表面化した実体験、そして npm: command not found というお約束のハマりどころまで、運用目線でまとめます。
[目次を開く]
1. huskyを入れたくない場面
git hooksの共有といえばhuskyが定番ですが、筆者がhuskyを見送ったのには理由があります。
まず、やりたいことに対して依存が重いこと。今回やりたかったのは「push前に typecheck と test を必ず通す」、それだけです。シェルスクリプト数行の仕事のために、devDependenciesを増やしてバージョン追従の面倒を抱えるのは割に合わないと感じました。huskyはメジャーバージョンごとにセットアップ方法が変わってきた経緯もあり、数年スパンで放置されがちな社内ツールやSDK的なリポジトリでは、その追従コスト自体がリスクになります。
そしてもう1つ。huskyが内部でやっていることの本質は、結局 core.hooksPath の設定です。それなら最初からGit標準機能を直接使えばよい、というのが今回の判断でした。
2. 仕組みの全体像
構成要素は3つだけです。
-
.githooks/ディレクトリ: フックスクリプトをリポジトリ内に置き、Gitで管理する -
git config core.hooksPath .githooks: Gitがフックを探す場所を、デフォルトの.git/hooks/(Git管理外)からリポジトリ内の.githooks/に切り替える -
package.jsonのprepareスクリプト:npm install時に自動実行されるライフサイクルスクリプトで、上記の設定を全員のローカルに適用する
流れを図にするとこうなります。
flowchart TD
A["npm install"] --> B["prepareスクリプト自動実行"]
B --> C["git config core.hooksPath .githooks"]
C --> D["git push 実行"]
D --> E[".githooks/pre-push が発火"]
E --> F["typecheck と test を実行"]
F -->|"成功"| G["pushが通る"]
F -->|"失敗"| H["pushを中断"] ポイントは、git config はリポジトリごとのローカル設定なので、clone しただけでは適用されないことです。そこを prepare で埋めるのがこの構成の肝です。Node.jsプロジェクトなら誰でも最初に npm install を叩くので、そのタイミングに設定を相乗りさせます。
3. 最小構成の手順(コピペ可能な完成形)
3-1. package.json に prepare を追加
"scripts": {
"prepare": "git config core.hooksPath .githooks"
} 3-2. .githooks/pre-push を作成
PATH対策(後述のハマりどころ)まで込みの完成形です。
#!/bin/sh
# pre-push: push前に typecheck と test を必ず通す
# 緊急時は git push --no-verify で回避可能
# nvm管理のNodeを使う環境向けのPATH対策(後述)
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
echo "[pre-push] typecheck & test を実行します..."
if ! npm run typecheck; then
echo "[pre-push] typecheck に失敗したため push を中断しました"
exit 1
fi
if ! npm test; then
echo "[pre-push] test に失敗したため push を中断しました"
exit 1
fi
echo "[pre-push] OK" 3-3. 実行権限を付けてコミット
chmod +x .githooks/pre-push
git add .githooks package.json
git commit -m "chore: pre-pushフックを core.hooksPath 方式で導入" 実行権限(chmod +x)を忘れるとフックが黙ってスキップされるので要注意です。権限情報はGitが記録するので、一度コミットすればチーム全員に引き継がれます。
なお、緊急時は git push --no-verify でフックを回避できます。「絶対に回避できないゲート」にしてしまうと、本番障害対応などの緊急時にチームがフック自体を憎み始めます。回避手段があることをREADMEなどに明記しておくのが、運用上はむしろ大事だと考えています。
4. 検証ゲートを立てたら隠れバグが出てきた話
このフック導入と同時に、CI側にも同じ typecheck と test のジョブを追加しました。それまでこのプロジェクトのCIはtypecheckのみで、テストは一切実行されていなかったのです。
すると導入した途端、既存コードから型エラーが3件表面化しました。今まで誰も気づかなかったものです。
そのうち1件は単なる型注釈の不備ではなく、実バグでした。unionのリテラル型で定義された値に対して、その型に存在しない値と比較する条件分岐があり、TypeScript的にはその比較が常にfalse——つまりその分岐は絶対に実行されないデッドブランチになっていたのです。コードレビューでは誰も気づかず、「動いているように見える」まま本番に居座っていました。
この経験から得た教訓はシンプルです。「完了した」の判定は人の目ではなく、検証ゲートに出させる。人間のレビューは「意図の妥当性」を見るのには強い一方、「型の整合が全経路で取れているか」のような機械的な検証は圧倒的に機械のほうが得意です。フックとCIの二段構えでゲートを立てるだけで、レビューの見落としが構造的に拾えるようになりました。
5. ハマりどころ: pre-pushが「npm: command not found」で落ちる
導入後、一部の環境でpushしようとすると npm: command not found でフックが落ちる、という報告がありました。
原因はフックの実行環境にPATHが通っていないことです。GUIのGitクライアントやエディタ統合からpushした場合、フックはログインシェルの初期化ファイル(.bashrc や .zshrc)を読まずに実行されることがあります。特にnvmでNode.jsを管理していると、npm へのパスは .zshrc 内のnvm初期化で通しているため、フックからは見えません。
ここでやってはいけないのが「じゃあ --no-verify で回避しよう」を常態化させることです。直すべきはゲートではなくPATH側です。前掲のスクリプトのように、フック内でnvmを明示的に読み込むのが確実です。
# nvmを明示的に読み込む
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" nvmを使っていない環境でもこの2行は無害(nvm.sh が無ければ何もしない)なので、チーム共有のフックには入れておいて損はありません。
6. huskyとの使い分け
hooksPath方式にもデメリットは正直にあります。
- グローバルフックと併用できない:
core.hooksPathを設定すると.git/hooks/もグローバル設定のフックも見なくなります。個人でグローバルフックを運用している人がいるチームでは衝突します -
npm installが走るまで未設定のまま: clone直後にinstallせずに作業を始めると、フックなしでpushできてしまいます。だからこそCI側にも同じ検証を置く二段構えが必須です - エコシステムの支援がない: lint-stagedとの連携などをリッチに組みたい場合、huskyのドキュメントとコミュニティ資産は魅力です
目安としては、「push/commit時に決まったコマンドを回すだけ」ならhooksPath方式で十分。フック運用自体を積極的に育てたいならhusky、という使い分けが現実的だと思います。なお今回はpre-pushを例にしましたが、同じ仕組みで .githooks/pre-commit を置けばcommit時のlint(lint-staged的な運用)にもそのまま応用できます。
CIの整備とセットで考えたい方は、CI/CDとは?基本概念から実践までも併せてどうぞ。
7. よくある質問
Q. フックはテスト失敗時に本当にpushを止められますか?
はい。pre-pushフックが非0で終了するとGitはpushを中断します。ただしローカルフックはあくまで第一関門で、--no-verify やinstall前の環境では素通りするため、最終防衛線はCIに置くのが原則です。
Q. チームメンバーに追加の設定作業は必要ですか?
不要です。prepare スクリプトが npm install 時に自動で core.hooksPath を設定するので、普段どおりcloneしてinstallすれば適用されます。逆に言うと、installを飛ばすと未適用になる点だけ周知が必要です。
Q. pnpmやyarnでも使えますか?
使えます。prepare はnpm由来のライフサイクルスクリプトですが、pnpm・yarnもinstall時に実行します(パッケージマネージャやバージョンによる挙動差はあるため、チームで使うツールでの動作確認はしておくと安心です)。
Q. フックのテスト実行が遅くてつらいときは?
pre-pushで回すのは「速くて確実な検証」(typecheck+ユニットテスト)に絞り、重いE2EなどはCIに寄せるのがおすすめです。ゲートが遅いと --no-verify の常用を招き、本末転倒になります。
8. まとめ
- 依存を増やさないgit hooksのチーム配布は、
.githooks/+git config core.hooksPath .githooks+prepareスクリプトの3点セットで実現できる - pre-pushで
typecheck && testを回すだけで、人のレビューが見落としていた型エラーや実バグ(デッドブランチ)が機械的に炙り出される。完了判定は検証ゲートに出させるのが要点 -
npm: command not foundはフック実行環境のPATH問題。ゲートを回避せず、フック内でnvmを読み込む等してPATH側を直す - clone直後は未設定になる等の弱点はCI側の同一検証でカバーする。フック運用を本格的に育てたい場合はhuskyも選択肢
Gitの基本的な開発フローから固めたい方は、Gitを使った開発手順も参考にしてください。小さな仕組みですが、「壊れたコードがpushされない」という安心感はチームの開発速度に直結します。まずはpre-push1本から試してみてください。
