複業4事業+本業を並行運用していると、リポジトリの数はあっという間に増える。私は今、コードを書くリポジトリだけで10近くを行き来している。今回はその中で地味に効いている改善、「git hooksをリポジトリごとに書くのをやめて、専用リポジトリからsubmoduleとして配布する仕組みに切り替えた話」を書く。
結論から言うと、main直push禁止・pre-commitでのlint実行・秘密情報スキャンといった「全リポジトリ共通で守りたいルール」をdev-guardrailsという1つのリポジトリに集約し、各プロジェクトへは.githooksとしてgit submodule導入するだけにした。これでルールを直したいときの修正箇所が1箇所になり、新規リポジトリのセットアップも数分で終わるようになった。
なぜ最初は各リポジトリに個別実装していたのか
複業を並行運用する以前、リポジトリが1〜2個だった頃は、.git/hooksにスクリプトを直接置くか、リポジトリ内にscripts/pre-commit.shのような形で持たせるかのどちらかで十分だった。困ることはなかった。
問題が表面化したのは、リポジトリが増えて「main直push禁止・ブランチ+PR+CI緑でマージ」というルールを複数プロジェクトで横展開し始めてからだ。あるリポジトリでpre-pushの禁止スクリプトを直したら、別のリポジトリには反映されない。新しいリポジトリを作るたびに、過去のリポジトリからスクリプトをコピペしてくる。コピペ元がどれが最新か分からなくなる。典型的な「車輪の再発明を繰り返す」状態に陥っていた。
決定的だったのは、複業の1つ(AI×BGM関連のリポジトリ)で、git hooksが必要になった際に独自スクリプトを書いてPRを出してしまい、その直後に別セッションで「共通化する」という方針を思い出してdev-guardrails導入のPRを出し、2つのPRが衝突した事故だった。同じことをやろうとして車輪を二重に作ってしまった。これで「共通の仕組みを先に作って、個別実装は絶対にしない」というルールを自分の中で確定させた。
submodule化の仕組み
やったことはシンプルだ。
dev-guardrailsという独立リポジトリを作り、hooks/配下にpre-commit・commit-msg・pre-pushの3種類のスクリプトを置く- 各プロジェクトのリポジトリで
git submodule addし、.githooksというディレクトリ名で参照する git config core.hooksPath .githooks/hooksでgitに「フックはここを見ろ」と教える- 各プロジェクトの
CLAUDE.md(またはREADME)に、新規clone後のセットアップ手順としてgit submodule update --initと上記configコマンドを明記する
フック側の設計で工夫したのは、lint・testの実行をプロジェクト側のpackage.jsonやMakefileに委ねる形にしたことだ。pre-commitスクリプト自体は「package.jsonにlintスクリプトが定義されていれば実行する、なければスキップする」というだけのロジックにしてある。こうすることで、TypeScriptプロジェクトでもPythonプロジェクトでも同じフックスクリプトがそのまま使い回せる。プロジェクト固有の実行コマンドをフック側にハードコードしないのがポイントだ。
pre-pushではmain直pushのブロックと秘密情報スキャンを共通化し、commit-msgではAIツールが自動で付与しがちなコミットトレーラーの禁止をチェックしている。これらはどのプロジェクトでも同じ判断基準でよいものばかりで、submodule化との相性が良かった。
導入して分かったメリットと注意点
実際に運用してみて感じたメリットは3つある。
- 修正が1箇所で全プロジェクトに波及する: pre-pushのルールを1つ変えたいとき、以前は関係するリポジトリの数だけPRを出していたが、今は
dev-guardrails側で直して各プロジェクトのsubmodule参照を更新するだけで済む - 新規リポジトリの立ち上げが速い: 新しいリポジトリを作る際、「ガードレールをどう入れるか」を毎回考える必要がなくなった。submodule追加とconfig設定の2コマンドで完了する
- ルールの一貫性が担保される: リポジトリごとに微妙にルールがずれる(あるリポジトリだけmain直pushが許されているなど)事故がなくなった
一方で注意点もある。submoduleはgitの機能として決して直感的ではなく、「submoduleの中身を更新したのに、参照元のリポジトリでcommitし忘れてsubmoduleのポインタが古いままになる」という事故は起きやすい。実際、dev-guardrails側を更新した後、それを使っている複業側のプロジェクトでgit submodule updateを忘れて「新しいフックが動いていない」状態にしばらく気づかなかったことがあった。submoduleを更新したら、それを使っている全プロジェクトへの反映を忘れずに行うという運用ルールを自分の中に追加で持つ必要がある。
もう1つ、フックの共通化とは別に、「事業運営の原則」(コンテンツマーケティングのNG行動集など、コードではなく判断基準のドキュメント)も同じ発想でsubmodule化してbiz-guardrailsという別リポジトリに切り出した。コードの品質と事業判断の原則は性質が違うので、あえてリポジトリを分けている。「共通化したいルールが増えてきたら、性質ごとに専用リポジトリを作ってsubmodule配布する」というパターンが自分の中で型になりつつある。
まとめ
- git hooksを各リポジトリに個別実装していたら、修正の反映漏れと車輪の再発明(同じ仕組みを2重に作る事故)が起きた
dev-guardrailsという専用リポジトリに集約し、各プロジェクトへ.githooksとしてsubmodule導入・core.hooksPathで有効化する構成に切り替えた- pre-commitのlint実行はプロジェクト側のスクリプト定義に委ね、フック自体はプロジェクト非依存に保つのが横展開のコツ
- submoduleは更新忘れが起きやすいので、共通リポジトリを更新したら利用中の全プロジェクトへの反映を運用ルールとして持つ必要がある
次回は、コード品質のガードレールとは別に切り出した「事業運営の原則」リポジトリ(biz-guardrails)について、なぜ強制発火の仕組みを持たせずに軽量な配布方式にしたのかを書く。
関連記事
- Node.js 22のネイティブTypeScript実行で捨てたツールたち
- Claude Codeで複数事業のコードベースを横断運用する方法
- 複数事業を並行運営するときのタスク管理術(GitHub Projectsでの一元化)
この実践記録はX(@amazenpapa)でも発信しています。