本文へ移動
購読
GUIDE#devtools更新 2026.09.24

pre-commitでコミット前チェックを自動化する:設定ファイルとCI連携

pre-commit 4.6でruff・shellcheck・gitleaks・pytestをコミット前とpush前に動かす設定、止まったときの直し方、GitHub Actionsでの全ファイル検査まで。

level
初級
time
約25分
stack
pre-commit 4.6 / Git / Python 3.12 / GitHub Actions
author
@ryoma_t
// 前提条件
Gitでコミットとpushができること / Pythonのツールを入れられること(uvかpipx)
pre-commitのフックが1回目のコミットで整形と修正を行い、git add -uのあと全フックがPassedになってコミットできたターミナル
画像:nagoya.dev(実際の画面・実行結果をもとに作成)

pre-commitとは、Gitのコミット直前に動く「フック」を、リポジトリに置いた.pre-commit-config.yamlで管理するためのツールです。末尾の空白やYAMLの構文、Pythonやシェルスクリプトの静的解析、秘密情報の混入といったチェックを、各ツールの実行環境の用意まで含めて自動で行い、問題があればコミットを止めます。

以下は、執筆時点の最新版pre-commit 4.6.2で、Python・シェルスクリプト・YAMLが混ざった自動化スクリプト用のリポジトリを想定して設定し、実際にコミットを止めたときの出力です。設定を書いてみて初めて気づいた落とし穴(gitleaksを置くステージなど)も含めています。

この記事でわかること

  • Gitフックとpre-commitの関係、フックごとに独立した実行環境が作られる仕組み
  • ruff・shellcheck・gitleaks・ローカルのpytestを使う.pre-commit-config.yamlの書き方
  • 自動修正されたファイルの扱いと、SKIP・--no-verifyでの一時的な回避
  • GitHub Actionsで同じチェックを全ファイルに走らせる設定
  • stagesの指定、autoupdateの挙動、huskyとの衝突など運用上の注意

pre-commitとは:Gitフックとの関係と仕組み

Gitには、コミットやpushの前後に.git/hooks/のスクリプトを実行する仕組みがあり、pre-commitという名前のスクリプトが0以外で終了するとコミットは作られません。ただし.git/の中身はリポジトリで共有されないため、手書きのフックはメンバーごとに配る必要があり、使うツールのインストールも各自に任せることになります。

この配布と環境の準備を引き受けるのがpre-commitです。pre-commit installは.git/hooks/pre-commitに小さな起動用スクリプト(先頭に# File generated by pre-commit: https://pre-commit.comと書かれたもの)を置くだけで、中身のチェックはリポジトリにコミットした設定ファイルから読みます。各フックのリポジトリは~/.cache/pre-commitにcloneされ、Pythonなら仮想環境、Go製のツールならGoのツールチェーンごと、フック専用の環境が自動で作られます。対象になるのは、既定ではそのコミットに含まれる(ステージされた)ファイルだけです。

方式設定の共有チェック用ツールの準備向いている場面
.git/hooksに手書き共有されない(各自でコピー)各自がインストール個人のリポジトリで1つだけ確認したい
pre-commit.pre-commit-config.yamlをコミットフックごとに自動で環境を作る(Python・Node.js・Goなど)言語が混在するリポジトリ、インフラのコード
huskypackage.jsonと.husky/をコミットnpmで入れたツールを使うNode.js中心のフロントエンドのリポジトリ

pre-commitのインストールと有効化

pre-commit自体はPython製のCLIなので、プロジェクトの依存に混ぜず、uvのuv tool installやpipxで単独のツールとして入れます。Ubuntu 24.04のaptでも入りますが、版は3.6.2で、後述のlanguage: unsupportedなどの新しい書き方を知りません。

terminal
uv tool install pre-commit        # または pipx install pre-commit
pre-commit --version              # pre-commit 4.6.2(検証時)
cd ~/infra-scripts                # 対象のリポジトリ
pre-commit install --install-hooks
出力の先頭2行(--install-hooksでは続けて各フックの環境作りのログが出る)
pre-commit installed at .git/hooks/pre-commit
pre-commit installed at .git/hooks/pre-push

2行出ているのは、後述の設定でdefault_install_hook_typesにpre-pushも指定しているためです。--install-hooksは各フックの環境作りもこの時点で済ませるオプションで、最初のコミットが待たされません。pre-commit installはcloneしたリポジトリごとに1回必要で、これを忘れたメンバーの手元ではチェックが動かない点は覚えておきます(そのためにCIでも同じチェックを走らせます)。

.pre-commit-config.yamlの書き方

.pre-commit-config.yaml
# .pre-commit-config.yaml
default_install_hook_types: [pre-commit, pre-push]
default_stages: [pre-commit]
exclude: ^vendor/

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
        args: ["--maxkb=500"]
      - id: check-merge-conflict
      - id: detect-private-key

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.16.8
    hooks:
      - id: ruff-check
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/shellcheck-py/shellcheck-py
    rev: v0.11.0.1
    hooks:
      - id: shellcheck

  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.30.1
    hooks:
      - id: gitleaks

  - repo: local
    hooks:
      - id: pytest
        name: pytest (push前のみ)
        entry: python -m pytest -q
        language: system
        pass_filenames: false
        always_run: true
        stages: [pre-push]

reposの各要素は「どのリポジトリの、どの版(rev)の、どのフック(id)を使うか」です。revはタグかコミットのハッシュで固定し、同じ設定ならメンバー全員が同じ版のツールで検査します(版はすべて執筆時点の最新)。最初のリポジトリ(pre-commit-hooks)の6つは言語を問わない基本的な検査で、check-added-large-filesは500KBを超えるファイルの追加を止めます。ruffはPythonの静的解析とフォーマット、shellcheckはシェルスクリプトの検査、gitleaksはトークンなどの秘密情報の検出を受け持ちます。

最後のrepo: localは、リポジトリの中や手元にあるコマンドをそのままフックにする書き方です。テストのように時間がかかる処理は、stages: [pre-push]でpushの前だけに回し、コミットのたびには走らせません。language: systemは、pre-commit 4.4以降ではunsupportedという名前に改められ、旧名は内部で読み替えられます。aptの3.6.2のような古い版が混ざるチームではsystemのまま、全員が4.4以上ならunsupportedと書きます。

default_stagesを書かないと、stagesを指定していないフックは既定ですべてのステージで動き、pushのたびにも全チェックが走ります。ただしcheck-added-large-filesのように、フック側で対象ステージを定義しているものはdefault_stages: [pre-commit]でも上書きされず、検証ではpush時にも実行されました。

コミットが止まったときの流れ

わざと問題を仕込んだファイル(使っていないimport、行末の空白、末尾に改行のないREADME)をコミットすると、次のように止まります。

terminal
git add .
git commit -m "バックアップスクリプトを追加"
git add -u
git commit -m "バックアップスクリプトを追加"
出力(1回目のgit commit)
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook

Fixing backup.py

fix end of files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook

Fixing README.md

check yaml...............................................................Passed
check for added large files..............................................Passed
check for merge conflicts................................................Passed
detect private key.......................................................Passed
ruff check...............................................................Failed
- hook id: ruff-check
- files were modified by this hook

Found 2 errors (2 fixed, 0 remaining).

ruff format..............................................................Failed
- hook id: ruff-format
- files were modified by this hook

1 file reformatted, 2 files left unchanged

shellcheck...........................................(no files to check)Skipped
Detect hardcoded secrets.................................................Passed

files were modified by this hookは、フックが問題を自動で直したという意味です。直したのは作業ツリーのファイルで、コミットに入るインデックスは直す前のままなので、git add -uで直した内容をステージし直してからもう一度コミットします。2回目は全フックがPassedになり、[main (root-commit) 1126a6d] バックアップスクリプトを追加とコミットが作られました。

git add -uは、フックが直したファイルだけでなく、Gitで管理しているファイルの未ステージの変更をすべてステージします。コミットに入れるつもりのなかった編集途中の変更まで一緒に入るので、別の作業を抱えているときはgit diffで差分を確かめ、フックが直したファイルだけを名前を指定してgit addします。

自動で直せない問題は、内容を読んで手で直します。YAMLの括弧の閉じ忘れはcheck-yamlがexpected ',' or ']', but got '<stream end>'と行番号つきで止め、シェルスクリプトのrsync -a $TARGET /backup/はshellcheckがSC2086 (info): Double quote to prevent globbing and word splitting.を示し、"$TARGET"と書き直す案まで表示しました。

秘密情報の検出:gitleaksを置くステージに注意

GitHubのトークンの形をした文字列を含むファイルをコミットしようとすると、gitleaksが値を伏せ字にしたうえで止めました。

出力の抜粋(トークンを含むファイルのコミット)
Detect hardcoded secrets.................................................Failed
- hook id: gitleaks
- exit code: 1

Finding:     github_token = "REDACTED
Secret:      REDACTED
RuleID:      github-pat
Entropy:     4.634184
File:        settings.ini
Line:        1
Fingerprint: settings.ini:github-pat:1

このフックの中身はgitleaks git --pre-commit --redact --staged --verboseで、--stagedのとおりステージされた変更だけを調べます。最初は「pushの前にだけ走らせれば十分」と考えてstages: [pre-push]を付けたところ、トークン入りのファイルがそのままPassedでpushされました。push時点ではステージされた変更が何もないためです。gitleaksは必ずpre-commitステージで動かします。

もう1つの注意は初回の重さです。gitleaksのフックはGo製なので、pre-commitはGoのツールチェーンごと環境を作ります。検証では、この環境作りを含む最初のpushに約44秒かかり、gitleaksを含む全フックの環境を作ったあとの~/.cache/pre-commitは421MBになりました。ディスクや時間が気になる環境では、gitleaksのリポジトリが用意している別のフックID、gitleaks-system(インストール済みのgitleaksを使う)やgitleaks-docker(コンテナで実行する)に切り替えられます。

一時的に止める・一部だけ動かす

terminal
SKIP=trailing-whitespace,ruff-format git commit -m "作業途中"   # 指定したフックだけ飛ばす
git commit --no-verify -m "緊急の修正"                          # フックをすべて飛ばす
pre-commit run --all-files                                     # 全ファイルを検査
pre-commit run shellcheck --files backup.sh                    # 1つのフックを1ファイルに

SKIPに書いたフックは出力でSkippedと表示され、残りは通常どおり動きます。--no-verifyはpre-commitに限らずGitのフックをすべて飛ばすので、検証でもフックの出力なしにコミットが作られました。どちらを使っても、CIで同じチェックを走らせていれば問題はプルリクエストの段階で見つかります。ローカルで飛ばすことを禁止するより、CIで必ず捕まえる設計のほうが現実的です。

CIでも同じチェックを走らせる:GitHub Actions

pre-commit installをし忘れたメンバーや、--no-verifyでのコミットに備えて、プルリクエストでは全ファイルを検査します。~/.cache/pre-commitを設定ファイルのハッシュをキーにしてキャッシュすると、2回目以降は環境作りを省けます。ワークフローの基本はGitHub Actionsで始めるCI/CDを参照してください(アクションの版は執筆時点の最新)。

.github/workflows/pre-commit.yml
name: pre-commit
on:
  pull_request:
  push:
    branches: [main]
permissions:
  contents: read
jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: "3.12"
      - uses: actions/cache@v6
        with:
          path: ~/.cache/pre-commit
          key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
      - run: pip install pre-commit
      - run: pre-commit run --all-files --show-diff-on-failure

--show-diff-on-failureは、フックが自動修正した差分をログに出すオプションです。検証ではAll changes made by hooks:の見出しに続けてgit diff形式の差分が表示されたので、CIのログを見るだけで何を直せばよいかがわかります。pre-commit runは既定でpre-commitステージのフックだけを動かすため、pushの前だけに回したpytestはここでは実行されません。テストはCIの別のジョブで走らせます。

運用の注意:autoupdate・huskyとの衝突・別実装

pre-commit autoupdateは、各リポジトリの既定ブランチからたどれる最新のタグへrevを書き換えます。リリース一覧の最新と一致するとは限らず、検証ではgitleaksがupdating v8.30.1 -> v8.30.0と古い版へ、shellcheck-pyがv0.11.0.1 -> v0.11.0.1-1へ書き換えられました。autoupdateの結果はそのままコミットせず、差分を読んでから取り込みます。--freezeを付けると、タグ名の代わりにコミットのハッシュで固定されます。

huskyなどがcore.hooksPathを設定しているリポジトリでは、pre-commit installが[ERROR] Cowardly refusing to install hooks with `core.hooksPath` set.で拒否されます。どちらか一方にまとめるのが基本で、Node.jsのプロジェクトでhuskyを残すなら、huskyのフックからpre-commit runを呼ぶ形にします。なお、執筆時点ではRustで書き直された互換実装prekも登場しており、検証では同じ.pre-commit-config.yamlのままprek installとprek run --all-filesが動きました。

つまずきポイント

症状・エラーメッセージ原因対処
files were modified by this hookが出て、コミットが作られないフックが作業ツリーのファイルを直したが、インデックスは直す前のまま差分を確認してgit add -uし、もう一度コミットする
[ERROR] Cowardly refusing to install hooks with `core.hooksPath` set.huskyなどがGitのフックの置き場所を変えているツールを1つにまとめる。寄せる先がpre-commitならgit config --unset-all core.hooksPath
トークン入りのファイルがgitleaksをPassedで通過したgitleaksをstages: [pre-push]で動かしていた。ステージされた変更しか見ないstagesを外し、pre-commitステージで動かす
初回のコミットで[INFO] This may take a few minutes...のまま長く待たされるフックごとの環境作り(gitleaksならGoの取得)が走っているpre-commit install --install-hooksで先に済ませる。重いフックはgitleaks-systemなどに切り替える

動作確認

設定を変えたら、全ファイルに対して一度走らせ、すべてPassed(対象ファイルがないものはSkipped)になることを確かめます。フックの起動用スクリプトが置かれているかも見ておきます。

terminal
$ pre-commit run --all-files
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
check yaml...............................................................Passed
check for added large files..............................................Passed
check for merge conflicts................................................Passed
detect private key.......................................................Passed
ruff check...............................................................Passed
ruff format..............................................................Passed
shellcheck...............................................................Passed
Detect hardcoded secrets.................................................Passed
$ head -2 .git/hooks/pre-commit
#!/usr/bin/env bash
# File generated by pre-commit: https://pre-commit.com

よくある質問

Pythonのpre-commitとは?

Pythonで書かれたGitフックの管理ツールで、pip install pre-commitやuv tool install pre-commitで入ります。本体はPython製ですが、フックはPython専用ではなく、Node.js・Go・Rustなどのツールもそれぞれの実行環境を自動で用意して動かせるので、シェルスクリプトやTerraformのリポジトリでも使えます。

Gitでpre-commitを無視するにはどうすればいいですか?

その1回だけすべてのフックを飛ばすならgit commit --no-verify(短縮形は-n)、特定のフックだけ飛ばすならSKIP=フックのID git commitを使います。恒久的にやめるならpre-commit uninstallです。飛ばしたチェックはCIで同じものを走らせて拾う前提で使ってください。

Precommit installとは?

リポジトリの.git/hooks/pre-commitに、pre-commitを呼び出す起動用スクリプトを書き込むコマンドです。.git/はGitで共有されないため、cloneするたびに各自が1回実行する必要があります。各フックの実行環境の準備まで同時に済ませたいときは--install-hooksを付けます。

Githubのpre-commitとは何ですか?

GitHub側にpre-commitの機能があるわけではなく、フックは各自のPCでコミットするときに動きます。GitHubで同じチェックを強制したい場合は、GitHub Actionsでpre-commit run --all-filesを実行し、ブランチ保護でそのジョブの成功をマージの条件にします。ホスト型のサービスとしてpre-commit.ciもあります。

まとめ

pre-commitは、設定ファイルをコミットし、各自がpre-commit install --install-hooksを1回実行するだけで、全員に同じ版のチェックを配れます。自動修正のあとはgit add -uで取り込み直し、テストのような重い処理はpre-push、gitleaksは必ずpre-commitに置きます。ローカルで飛ばされても困らないよう、CIで--all-filesの検査を必須にしておくのが仕上げです。

次に読む

$ subscribe --weekly
毎週月曜、企業ITとホームラボの要点を5分で。