pre-commit と gitleaks でシークレットのコミットを止める

pre-commit と gitleaks でシークレットのコミットを止める

AI エージェントが生成したコードにシークレットが混ざることがあります。pre-commit と gitleaks で、コミット直前に検出して止める仕組みを検証しました。
2026.10.17

コーヒーが好きな emi です。

Claude Code などの AI エージェントに開発を任せていると、生成したコードに API キーやパスワードが混ざったままコミットしようとする可能性があります。

その対策として、コミットの直前でシークレットを検出して止める仕組みである pre-commit と gitleaks を試してみました。

先に結論

pre-commit で gitleaks を呼ぶように設定しておくと、シークレットを含むファイルを git commit しようとした瞬間に検出され、コミットが中止されました。

gitleaks と pre-commit

私は普段開発業務を多く実施しないため、自分のためにもこれらのツールがどういったものなのか概要を記載します。

gitleaks

gitleaks は、ソースコードやコミット履歴から API キーやパスワードなどのシークレットを検出するツールです。

  • コードやコミットをスキャンして、シークレットらしき文字列を見つけると検出結果として報告する
  • シークレットを検出すると終了コード 1 を返すので、それを受けて pre-commit がコミットを中止できる

https://github.com/gitleaks/gitleaks

Git の hook

Git には hook という機能があります。commit や push などのタイミングで、決まったスクリプトを自動で走らせる仕組みです。

https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
https://git-scm.com/docs/githooks

以下のような種類の hook があります。※ここに挙げたのは一部で、他にもあります。

  • pre-commit hook
    • コミット確定の直前に走る hook。ここで何かがエラーを返すと commit が中止される。シークレットが混ざっていたら commit を止めるなど
  • commit-msg hook
    • コミットメッセージが入力された後に走る hook
  • pre-push hook
    • プッシュの直前に走る hook

work(作業ツリー)→ stage → commit → push という流れのどこで、どの hook が走るかのイメージが以下です。

commit-msg hook の用途
  • pre-commit hook: コミットメッセージを取得する前、コミットを作る前に走る

  • commit-msg hook: 提案されたコミットメッセージを保持するファイルを引数に受け取って走る

    • 順番は pre-commit hook → メッセージ入力 → commit-msg hook
  • commit-msg hook の用途

    • pre-commit hook はメッセージを受け取らないので、メッセージ自体を検査したいときに使う
    • コミットメッセージを標準フォーマットに正規化する
    • メッセージを検査し、問題があればコミットを拒否する
      • 終了コードが 0 以外ならコミット中止
    • Git 同梱のデフォルトは、重複した Signed-off-by 行を検出するとコミットを止める
    • 例
      • Conventional Commits(feat: や fix: など)の形式を強制する、チケット番号を必ず含めさせる

https://git-scm.com/docs/githooks

これらの hook の実体は、リポジトリの .git/hooks/ の下に置かれたシェルスクリプトです。ファイル名で走るタイミングが決まっていて、.git/hooks/pre-commit、.git/hooks/commit-msg、.git/hooks/pre-push のように置きます。拡張子は不要です。

リポジトリ/
└── .git/
    └── hooks/
        ├── pre-commit 
        ├── commit-msg 
        └── pre-push 
pre-commit のサンプル

git init でリポジトリを作ると、.git/hooks/ に Git 同梱のサンプルが .sample 付きで置かれます。pre-commit hook のサンプルは pre-commit.sample で、中身は次のようなシェルスクリプトです(コメントは一部省略)。

.git/hooks/pre-commit.sample
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments.  The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".

if git rev-parse --verify HEAD >/dev/null 2>&1
then
	against=HEAD
else
	# Initial commit: diff against an empty tree object
	against=$(git hash-object -t tree /dev/null)
fi

# (中略)非 ASCII のファイル名が追加されようとしていたら exit 1 で止める

# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --

このサンプルは、非 ASCII のファイル名や行末の空白エラーを見つけると exit 1(0 以外)で終了し、コミットを止めます。
コメントに「コミットを止めたいときは 0 以外のステータスで終了する」「このフックを有効にするにはファイル名を pre-commit にリネームする」旨が書かれています。
使うときは拡張子 .sample を外して pre-commit にしてくださいってことですね。

.git/ はリポジトリのローカルな作業用ディレクトリで、git clone や git push の対象に含まれません。そのため、この場所に直接置いたスクリプトはチームで共有できず、各自が手で用意する必要があります。

pre-commit(ツール)

名前がややこしいのですが、Git の pre-commit hook とは別に、pre-commit という名前のツールがあります。pip で入れる Python 製のプログラムで、pre-commit hook で走らせる検査の管理を楽にしてくれるものです。

https://pre-commit.com/

pre-commit(ツール)は、pre-commit hook から呼び出されると、設定ファイル(.pre-commit-config.yaml)に書いた検査(今回だと gitleaks)を実行します。

素の pre-commit hook との違い

「コミット前に gitleaks でシークレットを検出する」を実現する流れを比べます。

素の pre-commit hook の場合

素の pre-commit hook でやる場合は、.git/hooks/pre-commit に検査の中身を自分で直接書きます。

.git/hooks/pre-commit(自分で書く)
#!/bin/sh
# gitleaks を自分でインストールしておく必要がある
gitleaks protect --staged || exit 1

この方式には難点があります。

  • gitleaks を各自が別途インストールしておく必要がある
  • .git/hooks/ は git clone や git push に含まれないので、このスクリプトはチームで共有できず、メンバー各自が手で置く必要がある
  • バージョンや書き方が人によってバラつきやすい
pre-commit(ツール)を使う場合

pre-commit(ツール)を使う場合、自分で用意するのは .pre-commit-config.yaml という設定ファイル 1 つだけです。.pre-commit-config.yaml には、使う検査ツール(gitleaks など)とそのバージョンだけを書きます。

書く内容は次のとおりです。

.pre-commit-config.yaml(自分で書く)
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.30.1
    hooks:
      - id: gitleaks

.pre-commit-config.yaml を置いたら、pre-commit install を一回だけ実行します。これは最初の一回だけでよく、実行すると pre-commit(ツール)が .git/hooks/pre-commit というスクリプトを作ります。生成されるファイルの中身は以下です。

.git/hooks/pre-commit(pre-commit install が自動生成)
#!/usr/bin/env bash
# File generated by pre-commit: https://pre-commit.com

# start templated
INSTALL_PYTHON=/usr/bin/python3
ARGS=(hook-impl --config=.pre-commit-config.yaml --hook-type=pre-commit)
# end templated

HERE="$(cd "$(dirname "$0")" && pwd)"
ARGS+=(--hook-dir "$HERE" -- "$@")

if [ -x "$INSTALL_PYTHON" ]; then
    exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}"
elif command -v pre-commit > /dev/null; then
    exec pre-commit "${ARGS[@]}"
else
    echo '`pre-commit` not found.  Did you forget to activate your virtualenv?' 1>&2
    exit 1
fi

このスクリプトでやっているのは pre-commit(ツール)を呼び出すことだけです。--config=.pre-commit-config.yaml で読む設定ファイルを渡しています。

以降は git commit のたびに、次の流れが自動で動きます。

  1. git commit が .git/hooks/pre-commit(スクリプト)を実行する
  2. スクリプトが pre-commit(ツール)を呼び出す
  3. pre-commit(ツール)が .pre-commit-config.yaml を読む
  4. 設定ファイルに書かれた gitleaks を実行する

この方式だと、素の pre-commit hook の難点が解消されます。

  • gitleaks を自分でインストールしなくても、pre-commit が設定ファイルの repo から取得してくれる
  • .pre-commit-config.yaml はリポジトリのルート(.git/ と同じ階層)に置くので、リポジトリと一緒にコミットできる。.git/hooks/ に直接置く方式と違い、設定をチームで共有できる
  • rev でバージョンを固定できるので、メンバー間で同じ検査が走る

ややこしいですね。まとめると以下です。

  • pre-commit install のとき(最初の一回)
    • pre-commit(ツール)が .git/hooks/pre-commit(スクリプト)を作る
  • git commit のとき
    • git commit が.git/hooks/pre-commit(スクリプト)を呼び、.git/hooks/pre-commit(スクリプト)が pre-commit(ツール)を呼ぶ
    • pre-commit(ツール)が .pre-commit-config.yaml を読んで gitleaks を実行する

検証で使ったフォルダ構成

今回は使い捨ての検証用リポジトリ lab/ を作って試しました。フォルダ構成は以下です。

(作業用フォルダ)/
└── lab/                          ← 検証用に git init したリポジトリ
    ├── .git/
    │   └── hooks/
    │       └── pre-commit        ← pre-commit install で自動生成される
    ├── .pre-commit-config.yaml   ← 自分で書く設定ファイル(リポジトリのルート)
    ├── config.py                 ← シークレットを混ぜた検証用ファイル
    └── README.md

動作環境

  • WSL 上の Ubuntu
  • Git 2.34.1
  • pre-commit 4.6.2(pip で導入)
  • gitleaks v8.30.1(pre-commit 経由で取得)
  • Claude Code v2.1.248

gitleaks は自分で別途インストールせず、pre-commit に取得を任せています。.pre-commit-config.yaml に gitleaks のリポジトリを書いておくと、実行時に取得してくれます。

手順1 インストール状況を確認する

まず、いまの環境に gitleaks と pre-commit が入っていないか、あわせて導入手段の候補(apt / snap / go / Homebrew / pip)が使えるかを確認しました。

echo "=== gitleaks ==="; which gitleaks; echo "=== pre-commit ==="; which pre-commit; echo "=== pipx ==="; which pipx; echo "=== pip3 ==="; which pip3; echo "=== go ==="; which go && go version; echo "=== snap ==="; which snap; echo "=== brew ==="; which brew; echo "=== apt-cache gitleaks ==="; apt-cache policy gitleaks 2>/dev/null | head -n 5

▼実行結果

emiki@<hostname>:~/work/xx$ echo "=== gitleaks ==="; which gitleaks; echo "=== pre-commit ==="; which pre-commit; echo "=== pipx ==="; which pipx; echo "=== pip3 ==="; which pip3; echo "=== go ==="; which go && go version; echo "=== snap ==="; which snap; echo "=== brew ==="; which brew; echo "=== apt-cache gitleaks ==="; apt-cache policy gitleaks 2>/dev/null | head -n 5
=== gitleaks ===
=== pre-commit ===
=== pipx ===
=== pip3 ===
/usr/bin/pip3
=== go ===
=== snap ===
/usr/bin/snap
=== brew ===
=== apt-cache gitleaks ===
emiki@<hostname>:~/work/xx$ 

gitleaks も pre-commit も未導入で、pip3 は使える状態でした。今回は pip で pre-commit を入れる方針にしました。

手順2 pre-commit を入れる

pip で pre-commit を入れます。

pip3 install --user pre-commit
実行結果
emiki@<hostname>:~/work/xx$ pip3 install --user pre-commit
Collecting pre-commit
  Downloading pre_commit-4.6.2-py2.py3-none-any.whl (226 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 226.2/226.2 KB 2.1 MB/s eta 0:00:00
Collecting identify>=1.0.0
  Downloading identify-2.6.19-py2.py3-none-any.whl (99 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 99.4/99.4 KB 7.8 MB/s eta 0:00:00
Collecting virtualenv>=20.10.0
  Downloading virtualenv-21.7.11-py3-none-any.whl (5.3 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 5.3/5.3 MB 24.9 MB/s eta 0:00:00
Requirement already satisfied: pyyaml>=5.1 in /usr/lib/python3/dist-packages (from pre-commit) (5.4.1)
Collecting nodeenv>=0.11.1
  Downloading nodeenv-1.10.0-py2.py3-none-any.whl (23 kB)
Collecting cfgv>=2.0.0
  Downloading cfgv-3.5.0-py2.py3-none-any.whl (7.4 kB)
Collecting distlib<1,>=0.3.7
  Downloading distlib-0.4.3-py2.py3-none-any.whl (470 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 470.6/470.6 KB 12.4 MB/s eta 0:00:00
Collecting python-discovery>=1.6
  Downloading python_discovery-1.6.0-py3-none-any.whl (38 kB)
Collecting typing-extensions>=4.13.2
  Downloading typing_extensions-4.16.0-py3-none-any.whl (45 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 45.6/45.6 KB 4.0 MB/s eta 0:00:00
Collecting filelock<4,>=3.24.2
  Downloading filelock-3.32.7-py3-none-any.whl (100 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.2/100.2 KB 7.1 MB/s eta 0:00:00
Collecting platformdirs<5,>=3.9.1
  Downloading platformdirs-4.11.9-py3-none-any.whl (24 kB)
Installing collected packages: distlib, typing-extensions, platformdirs, nodeenv, identify, filelock, cfgv, python-discovery, virtualenv, pre-commit
Successfully installed cfgv-3.5.0 distlib-0.4.3 filelock-3.32.7 identify-2.6.19 nodeenv-1.10.0 platformdirs-4.11.9 pre-commit-4.6.2 python-discovery-1.6.0 typing-extensions-4.16.0 virtualenv-21.7.11
emiki@<hostname>:~/work/xx$ 

~/.local/bin に入り、PATH も通っていました。

which pre-commit; pre-commit --version

▼実行結果

emiki@<hostname>:~/work/xx$ which pre-commit; pre-commit --version
/home/emiki/.local/bin/pre-commit
pre-commit 4.6.2
emiki@<hostname>:~/work/xx$ 

手順3 検証用のリポジトリを作る

使い捨ての検証用リポジトリを作ります。作業ディレクトリのパスは BASE という変数に入れておき、以降はそれを使い回します(パスは /path/to/project にマスクしています)。

BASE="/path/to/project"; mkdir -p "$BASE/lab" && cd "$BASE/lab" && git init -q && git config user.email "test@example.com" && git config user.name "test" && echo "# gitleaks lab" > README.md && git add README.md && git commit -qm "init" && echo "OK: lab created" && pwd && git log --oneline

▼実行結果

emiki@<hostname>:~/work/xx$ BASE="/path/to/project"; mkdir -p "$BASE/lab" && cd "$BASE/lab" && git init -q && git config user.email "test@example.com" && git config user.name "test" && echo "# gitleaks lab" > README.md && git add README.md && git commit -qm "init" && echo "OK: lab created" && pwd && git log --oneline
OK: lab created
/path/to/project/lab
a12414b (HEAD -> master) init
emiki@<hostname>:~/work/xx$ 

手順4 .pre-commit-config.yaml を作る

.pre-commit-config.yaml を作り、gitleaks を呼ぶように書きます。書き方は pre-commit の公式ドキュメントにまとまっています。

https://pre-commit.com/#pre-commit-configyaml---top-level

今回書いたのは次の内容です。

.pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.30.1
    hooks:
      - id: gitleaks
  • repos
    • 使う検査ツール(リポジトリ)の一覧。今回は gitleaks の 1 つだけ
  • repo
    • hook のコードを git clone してくる元のリポジトリ URL。gitleaks の GitHub リポジトリを指定
    • pre-commit がここから gitleaks を取得するので、gitleaks を自分で別途インストールしなくても使える
  • rev
    • gitleaks のコードを GitHub から取ってくるとき、どの時点のものを取るかの指定。rev は revision(リビジョン)
    • Git で「ある時点のコード」を指す書き方には、タグ名(例 v8.30.1)、コミットの SHA(例 84f01ac...)、ブランチ名や HEAD がある。このうちタグと SHA は中身が変わらないが、ブランチ名や HEAD は新しいコミットが入ると中身が変わる
    • rev に書けるのは、中身が変わらないタグか SHA。pre-commit は一度取ってきた gitleaks をキャッシュして使い回すため、main のようなブランチ名や HEAD(=変わるもの)は使えない
    • 今回はこの記事を書いた時点の最新リリースのタグ v8.30.1 を指定。タグで固定しておくと、いつ誰が実行しても同じ v8.30.1 の gitleaks が走る
  • hooks
    • そのリポジトリが提供する hook のうち、どれを使うかの一覧
    • 1 つのリポジトリが複数の hook を提供していることがあり、それぞれに id(名前)が付いている。repo で「どのリポジトリから取るか」を決め、hooks の id で「その中のどの検査を使うか」を選ぶ
    • id は自分で命名するものではなく、提供側のリポジトリが決めている。提供側のリポジトリのルートにある .pre-commit-hooks.yaml に提供する hook が id 付きで定義されているので、利用者はそこから使いたい id を調べて指定する
    • id
      • 使う hook の識別子(名前)。gitleaks のリポジトリの .pre-commit-hooks.yaml には gitleaks(通常版)、gitleaks-docker、gitleaks-system が定義されている。今回は通常版の id: gitleaks を指定

複数の検査を使いたい場合は、hooks の下に id を並べます。たとえば pre-commit/pre-commit-hooks という定番リポジトリから 2 つ使なら以下のように書きます。

pre-commit-config.yaml(複数の id を並べる例)
.pre-commit-config.yaml(複数の id を並べる例)
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: trailing-whitespace   # 行末の空白を消す検査
      - id: end-of-file-fixer     # ファイル末尾を整える検査

今回は gitleaks の 1 つだけなので、id も 1 つだけです。

https://pre-commit.com/#pre-commit-configyaml---repos

今回用意した .pre-commit-config.yaml は「コミットの直前に gitleaks の v8.30.1 を GitHub から取ってきて走らせてね」の意味です。

手順5 Git のフックに登録する

Git の hook に登録します。

pre-commit install を実行します。これで .pre-commit-config.yaml の内容が Git の pre-commit hook に登録され、.git/hooks/pre-commit が作られます。

あわせて ls -la .git/hooks/pre-commit も実行しています。.git/hooks/pre-commit が実際に作られたか、実行権限(先頭の -rwxr-xr-x)が付いているかを確認するためです。

BASE="/path/to/project"; cd "$BASE/lab" && pre-commit install && ls -la .git/hooks/pre-commit

▼実行結果

emiki@<hostname>:~/work/xx$ BASE="/path/to/project"; cd "$BASE/lab" && pre-commit install && ls -la .git/hooks/pre-commit
pre-commit installed at .git/hooks/pre-commit
-rwxr-xr-x 1 emiki emiki 598 Sep 18 09:42 .git/hooks/pre-commit
emiki@<hostname>:~/work/xx$ 

手順6 生成された hook の中身を見る

登録によって生成された .git/hooks/pre-commit が、本当に「pre-commit(ツール)を呼ぶだけのスクリプト」になっているか確認します。

BASE="/path/to/project"; cd "$BASE/lab" && cat .git/hooks/pre-commit

▼実行結果

emiki@<hostname>:~/work/xx$ BASE="/path/to/project"; cd "$BASE/lab" && cat .git/hooks/pre-commit
#!/usr/bin/env bash
# File generated by pre-commit: https://pre-commit.com
# ID: 138fd403232d2ddd5efb44317e38bf03

# start templated
INSTALL_PYTHON=/usr/bin/python3
ARGS=(hook-impl --config=.pre-commit-config.yaml --hook-type=pre-commit)
# end templated

HERE="$(cd "$(dirname "$0")" && pwd)"
ARGS+=(--hook-dir "$HERE" -- "$@")

if [ -x "$INSTALL_PYTHON" ]; then
    exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}"
elif command -v pre-commit > /dev/null; then
    exec pre-commit "${ARGS[@]}"
else
    echo '`pre-commit` not found.  Did you forget to activate your virtualenv?' 1>&2
    exit 1
fi
emiki@<hostname>:~/work/xx$ 

冒頭で見たとおり、検査そのものは書かれておらず、pre-commit(ツール)を呼び出しているだけになっています。

手順7 検出されるダミー値でファイルを用意する

Claude Code が AWS の認証情報を含む設定ファイルを作った、という想定で config.py を用意します。

config.py は、検証用リポジトリ lab/ のルートに置くシークレットを混ぜたファイルです。

フォルダ構成再掲
(作業用フォルダ)/
└── lab/                          ← 検証用に git init したリポジトリ
    ├── .git/
    │   └── hooks/
    │       └── pre-commit        ← pre-commit install で自動生成される
    ├── .pre-commit-config.yaml   ← 自分で書く設定ファイル(リポジトリのルート)
    ├── config.py                 ← シークレットを混ぜた検証用ファイル
    └── README.md

ここに入れるダミー値ですが、自分でランダムに作成すると、万が一どこかの誰かのシークレットと一致してしまった場合が嫌なので、gitleaks で公開済みのサンプル値を使います。

  • AKIAIMNOJVGFDXXXE4OA

この値が AWS アクセスキーとして検出される例は gitleaks のドキュメントにも載っています。

https://github.com/gitleaks/gitleaks

なお AWS の認証は Access Key ID と Secret Access Key の組み合わせで使います。Access Key ID は既知のサンプル値を使ったので、Secret 側は形式だけ似せた文字列を自分でランダムに作成しました。

config.py
AWS_ACCESS_KEY_ID = "AKIAIMNOJVGFDXXXE4OA"
AWS_SECRET_ACCESS_KEY = "kQ9pxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

実は検証の最初はよくあるサンプル値を入れていて、そのときはコミットが通ってしまいました。その件は後述します。

手順8 差し替えた値をステージする

私は普段あまり開発をしないので、ここで出てくる「ステージ」と「コミット」を自分のために整理しておきます。

Git では、変更したファイルをいきなり履歴に記録するのではなく、2 段階に分けて進めます。

  • git add(ステージ)
    • 「次の記録にこのファイルを含める」と選んで、ステージングエリアという待機場所に載せる操作
    • この時点ではまだ履歴には残らない。記録する対象を手前で選んでいる段階
  • git commit(コミット)
    • ステージに載せたファイルを、ひとまとまりの変更として履歴に記録する操作
    • ここで初めてリポジトリの履歴に残る

今回の pre-commit と gitleaks の検査は、この git commit(コミットの確定)の直前に走ります。なので、まず git add でファイルをステージに載せ、そのうえで git commit したときに検査が動く、という流れになります。

まず config.py と .pre-commit-config.yaml を git add してステージに載せます。

BASE="/path/to/project"; cd "$BASE/lab" && git add config.py .pre-commit-config.yaml && echo "---- staged ----" && git status

▼実行結果

emiki@<hostname>:~/work/xx$ BASE="/path/to/project"; cd "$BASE/lab" && git add config.py .pre-commit-config.yaml && echo "---- staged ----" && git status
---- staged ----
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   .pre-commit-config.yaml
        new file:   config.py

emiki@<hostname>:~/work/xx$ 

ここで config.py と一緒に .pre-commit-config.yaml も add しているのは、後半の余談で触れる試行錯誤の経緯があるためです。
※本来なら .pre-commit-config.yaml は先にコミット済みになっているのが自然です。

手順9 コミットが止まった

ステージした状態でコミットします。

BASE="/path/to/project"; cd "$BASE/lab" && git commit -m "add config"; echo "---- exit code: $? ----"

▼実行結果

emiki@<hostname>:~/work/xx$ BASE="/path/to/project"; cd "$BASE/lab" && git commit -m "add config"; echo "---- exit code: $? ----"
Detect hardcoded secrets.................................................Failed
- hook id: gitleaks
- exit code: 1

    ○
    │╲
    │ ○
    ○ ░
    ░    gitleaks

    Finding:     AWS_SECRET_ACCESS_KEY = "REDACTED"
    Secret:      REDACTED
    RuleID:      generic-api-key
    Entropy:     5.131556
    File:        config.py
    Line:        2
    Fingerprint: config.py:generic-api-key:2

    Finding:     ...WS_ACCESS_KEY_ID = "REDACTED
    Secret:      REDACTED
    RuleID:      aws-access-token
    Entropy:     3.646439
    File:        config.py
    Line:        1
    Fingerprint: config.py:aws-access-token:1

    11:45AM INF 0 commits scanned.
    11:45AM INF scanned ~212 bytes (212 bytes) in 295ms
    11:45AM WRN leaks found: 2

---- exit code: 1 ----
emiki@<hostname>:~/work/xx$ 

Failed になり、コミットが中止されました。gitleaks が終了コード 1 を返し、それを受けて git commit が失敗した、という流れです。

検出は 2 件でした。1 行目が Access Key ID(aws-access-token)、2 行目は Secret 側の文字列で、ランダムさが高いため汎用の API キー(generic-api-key)として拾われました。こちらは想定していなかったおまけですが、それらしい文字列も検出することの実例になりました。

手順10 本当に止まっているか確認する

本当にコミットが止まっているか確認します。

BASE="/path/to/project"; cd "$BASE/lab" && echo "=== log ===" && git log --oneline && echo "=== status ===" && git status

▼実行結果

emiki@<hostname>:~/work/xx$ BASE="/path/to/project"; cd "$BASE/lab" && echo "=== log ===" && git log --oneline && echo "=== status ===" && git status
=== log ===
a12414b (HEAD -> master) init
=== status ===
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   .pre-commit-config.yaml
        new file:   config.py

emiki@<hostname>:~/work/xx$ 
  • === log === の下(git log --oneline の出力)
    • コミット履歴を 1 行ずつ出すコマンド
    • 最初の init の 1 行しか出ていない
    • 手順 9 のコミットが成功していれば add config の行が増えているはずが、それが無いので commit されていないと分かる
  • === status === の下の Changes to be committed:(git status の出力)
    • 今の状態を見るコマンド
    • config.py と .pre-commit-config.yaml が new file に出ている
    • ファイルが git add でステージに載ったまま、まだ git commit されていない(履歴に記録されていない)状態

コマンド履歴には最初の init しか残っておらず、add config は入っていません。ファイルは手元に残ったまま、履歴には記録されていません。コミットの直前で止まったことが分かります。

余談 サンプル値だとコミットが通ってしまった

検証の際、最初はよくあるサンプル値を入れていました。そのときの config.py は以下です。

config.py
AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

AKIAIOSFODNN7EXAMPLE は AWS 公式ドキュメントでも使われるダミーのサンプル値です。

https://docs.aws.amazon.com/ja_jp/sdkref/latest/guide/feature-static-credentials.html

この値で config.py を作って git add し、コミットしてみました。

emiki@<hostname>:~/work/xx$ BASE="/path/to/project"; cd "$BASE/lab" && cat > config.py <<'EOF'
# Claude Code が生成したと想定した設定ファイル
AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
EOF
git add config.py && echo "OK: config.py staged"
OK: config.py staged
emiki@<hostname>:~/work/xx$ 
emiki@<hostname>:~/work/xx$ BASE="/path/to/project"; cd "$BASE/lab" && git commit -m "add config"; echo "---- exit code: $? ----"
[INFO] Initializing environment for https://github.com/gitleaks/gitleaks.
[INFO] Installing environment for https://github.com/gitleaks/gitleaks.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
Detect hardcoded secrets.................................................Passed
[master b850bb8] add config
 1 file changed, 3 insertions(+)
 create mode 100644 config.py
---- exit code: 0 ----
emiki@<hostname>:~/work/xx$ 

Passed になり、コミットが通ってしまいました。止めたかったのに止まりません。

いったんこのコミットは取り消します。

emiki@<hostname>:~/work/xx$ BASE="/path/to/project"; cd "$BASE/lab" && git reset --soft HEAD~1 && echo "---- reset done ----" && git status
---- reset done ----
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   config.py

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        .pre-commit-config.yaml

emiki@<hostname>:~/work/xx$ 

ここで git reset --soft HEAD~1 でコミットだけを取り消したため、一度 add 済みだった config.py はステージに残り、.pre-commit-config.yaml は未コミットのまま残りました。手順 8 で config.py と .pre-commit-config.yaml を一緒に git add していたのは、この経緯があったからです。

原因は、この値が gitleaks の除外対象だったことです。gitleaks の AWS 検出ルールには allowlist があり、.+EXAMPLE$(EXAMPLE で終わる値)を除外するようになっています。

https://github.com/gitleaks/gitleaks/blob/master/cmd/generate/config/rules/aws.go

AKIAIOSFODNN7EXAMPLE は、AWS 公式ドキュメントでよく「ここに自分のアクセスキーを入れてね」という穴埋めの見本として載せています。
この値がドキュメントやサンプルコードで使い回されるので、世の中のコードにあちこち現れます。
gitleaks からするとこれをいちいちシークレットとして検出すると、本物ではないのに警告が出る誤検知が増えてしまいます。
そのためこうしたサンプル値は、誤検知を避けるために allowlist で検出対象から除外される場合があります。GitLab の Issue でも、AWS のサンプルキーを検出対象から除外する提案がされています。

https://gitlab.com/gitlab-org/gitlab/-/work_items/452105

この後 gitleaks 自身の公開テスト値に差し替えたところ、想定通りコミットが止まる挙動を確認できました。

おわりに

pre-commit と gitleaks を入れておくと、シークレットを含むファイルのコミットが手元で止まりました。Claude Code にコードを書かせていて、うっかりシークレットが混ざっても、履歴に残る前に気づけます。

ただこの仕組みが守るのはあくまでコミットのタイミングだけです。

Claude Code がファイルを削除したり外部に通信したりといったコミット以外の危ない操作は gitleaks で止められないので、こうした操作を止める手段としては PreToolUse フックや sandbox が候補になります。

https://dev.classmethod.jp/articles/claude-code-sandbox-202604/

https://dev.classmethod.jp/articles/claude-code-hooks-aws-production-guard/

本記事への質問やご要望については画面下部の「DevelopersIOへのご意見」からお問い合わせいただけます。

参考

https://pre-commit.com/

https://github.com/gitleaks/gitleaks

https://dev.classmethod.jp/articles/claude-code-security-basics/

この記事をシェアする

関連記事