Claude Codeでgitleaksを使う — 秘密情報のコミットをhookで止める
Claude Codeが書いたコードにAPIキーが混ざる事故を、gitleaksとhooksで二段に止める設定です。exit 1が素通りする罠、onFailure、誤検知の除外まで扱います。
Claude Codeが書いたコードに秘密情報が混ざる事故は、gitleaksをgitのpre-commitとClaude CodeのPreToolUse hookの二段に置くと止められます。ただしgitleaksは検出時にexit 1を返し、Claude Codeのhookではexit 1がブロックになりません。ラッパーでexit 2に変換する一手間が要ります。
この記事では、その変換スクリプトを含めた設定一式と、誤検知や既存の履歴の扱いをまとめます。
gitleaksが止めるものと、二段に置く理由
gitleaksは、パスワード・APIキー・トークンのような秘密情報を、gitリポジトリ、ファイル、標準入力から検出するツールです。検出は正規表現が中心で、ルールごとにエントロピーのしきい値やキーワードを設定できます。
Claude Codeと組み合わせるときの狙いは、Claudeがファイルへ書いた値や、コミットに入った値を、リモートへ届く前に落とすことです。守る場所は2つに分けます。
二段の関門の役割分担
gitのpre-commit
人間が打ったコミットも、Claudeが実行したコミットも通ります。Claude Codeを使わない作業にも効くのが強みです。
PreToolUse hook
ファイルを書く前とコミットを打つ前に止めます。拒否理由がClaudeに返るので、その場で書き直させられます。
pre-commitだけでも秘密情報の流出は防げます。hookを足す価値は、失敗がコミット時ではなく書いた直後に分かり、Claudeが自分で直せる点にあります。
なお、gitleaksのREADMEには「機能は完成しており、新機能は取り込まない。今後のリリースはセキュリティパッチのみ。開発の軸はBetterleaksに移す」という趣旨の警告があります。既存の検出ルールで足りるかを見て導入を決めるのが現実的です。
gitleaksを入れてコマンドの形を押さえる
macOSならHomebrewで入れます。Dockerイメージ(zricethezav/gitleaksとghcr.io/gitleaks/gitleaks)や、goを入れた環境でのソースビルドも用意されています。
brew install gitleaks
gitleaks versionスキャンのモードは3つです。
| サブコマンド | 対象 | 使いどころ |
|---|---|---|
git | 対象gitリポジトリの履歴 | 使いどころ過去のコミットの棚卸し、--stagedでコミット前の差分 |
dir | 対象ディレクトリやファイル | 使いどころ作業ツリーの現状確認 |
stdin | 対象標準入力 | 使いどころhookに渡された文字列の検査 |
古い記事に出てくるgitleaks detectとgitleaks protectは、v8.19.0で非推奨になりました。コマンド自体は残っていますが、--helpには出ません。新しく書く設定はgit・dir・stdinで揃えます。
検出されたときの終了コードは、既定で1です。--exit-codeで変えられます。この「1」が、後で出てくる罠の正体です。
第一の関門 — gitのpre-commitで全コミットを検査する
公式のpre-commit定義は、gitleaks git --pre-commit --redact --staged --verboseを実行する内容です。pre-commitフレームワークを使っているなら、.pre-commit-config.yamlに次を足します。revはREADMEの例の値で、pre-commit autoupdateで最新に更新できます。
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.2
hooks:
- id: gitleakspre-commit installフレームワークを入れたくない場合は、.git/hooks/pre-commitに同じコマンドを置けば足ります。
#!/bin/sh
exec gitleaks git --pre-commit --redact --staged --verbosechmod +x .git/hooks/pre-commitで実行権限を付けます。コミットが止まれば成功です。READMEの例では、検出時にDetect hardcoded secrets....Failedと表示されます。
--redactは、検出した秘密情報をログと標準出力で伏せ字にするフラグで、既定では100%伏せます。ログや標準出力に値を残さないために付けておきます。
第二の関門 — PreToolUse hookで書き込みとコミット前に止める
Claude Codeのhookは、ツールが動く前にPreToolUseとして呼ばれ、exit 2を返すとツール呼び出しが止まります。このとき標準エラー出力がClaudeに渡るので、理由を書いておけば書き直しに使われます。
検査するもの
1つのスクリプトで3つの入口を見ます。
Writeのtool_input.content(書き込む全文)Editのtool_input.new_string(差し替える文字列)Bashでgit commitを含むコマンド(その時点のステージ済み差分)
スクリプト
.claude/hooks/gitleaks-guard.shとして保存します。
#!/bin/bash
input=$(cat)
tool=$(jq -r '.tool_name // empty' <<<"$input")
if ! command -v gitleaks >/dev/null 2>&1; then
echo "gitleaks が PATH にありません。brew install gitleaks で入れてください" >&2
exit 2
fi
block() {
echo "gitleaks が秘密情報らしき文字列を検出しました。" >&2
echo "値は環境変数かシークレット管理から読む形に書き換えてください。" >&2
echo "$1" >&2
exit 2 # gitleaks の exit 1 を、hook のブロック(2)に変換する
}
case "$tool" in
Write|Edit)
text=$(jq -r '.tool_input.content // .tool_input.new_string // empty' \
<<<"$input")
[ -z "$text" ] && exit 0
out=$(printf '%s' "$text" | gitleaks stdin --redact --no-banner 2>&1) \
|| block "$out"
;;
Bash)
cmd=$(jq -r '.tool_input.command // empty' <<<"$input")
case "$cmd" in
*"git commit"*"--no-verify"*)
echo "--no-verify はこのリポジトリでは使わないでください" >&2
exit 2
;;
*"git commit"*)
cd "${CLAUDE_PROJECT_DIR:-.}" || exit 2
out=$(gitleaks git --pre-commit --redact --staged --no-banner 2>&1) \
|| block "$out"
;;
esac
;;
esac
exit 0ポイントは3つあります。
gitleaksが検出すると終了コード1で終わりますが、Claude Codeではexit 0と2以外は「非ブロックのエラー」で、アクションが進んでしまいます。block関数で明示的にexit 2へ変換しています。- 検出結果は
--redact付きで標準エラーに流します。Claudeに返る文面に秘密情報そのものを載せないためです。 gitleaksが見つからないときもexit 2にします。道具がないまま素通りする状態を避けるためです。
settings.jsonへの登録
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/gitleaks-guard.sh",
"onFailure": "block"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/gitleaks-guard.sh",
"onFailure": "block"
}
]
}
]
}
}ifはパーミッションルールの書式でhookの起動を絞る項目で、Bash(git *)ならgitコマンドを含む呼び出しだけでスクリプトが走ります。ifの判定はベストエフォートで、判定できないときはhookが走る仕様です。そのため、スクリプト側でもgit commitかどうかを見直しています。
保存したらchmod +x .claude/hooks/gitleaks-guard.shを実行します。
素通りさせない設定 — exit codeとonFailure
hookの結果は、終了コードで次のように決まります。
| 終了コード | PreToolUseでの扱い |
|---|---|
| 0 | PreToolUseでの扱い成功。ツールは実行される |
| 2 | PreToolUseでの扱いブロック。標準エラーがClaudeに渡る |
| それ以外 | PreToolUseでの扱い非ブロックのエラー。ツールは実行される |
つまり、gitleaksの終了コード1をそのまま返すhookは、秘密情報を見つけても何も止めません。トランスクリプトにhook errorの通知が出るだけです。終了コードを自分で決めるスクリプトにしておく理由は、ここにあります。
もう1つの穴が、スクリプトが起動できない場合です。パスの打ち間違いやスクリプトの実行権限漏れで、hookは「失敗」します。既定ではこの失敗でもツールは進みます。"onFailure": "block"を付けると、起動失敗・タイムアウト・exit 0と2以外が、ブロックとして扱われます。このフィールドはv2.1.295以降で使えます。詳細はonFailureの設定にまとめています。
落とし穴
gitleaksの検出がhookでは通過扱いになる
gitleaks stdinの結果をそのままexit $?で返すと、検出時のexit 1が非ブロックのエラーになります。必ずexit 2に読み替えてください。
動作は、設定した直後に確かめます。ルールに合う形のダミーの鍵を、手元で標準入力に流します。
printf 'token = "ghp_%s"\n' "$(printf 'aB1cD2eF3gH4iJ5kL6mN7oP8qR9sT0uVwXyZ' | head -c 36)" \
| gitleaks stdin -v --redact; echo "exit=$?"検出されればexit=1になり、ルールIDと伏せ字の結果が表示されます。何も出ずにexit=0なら、ダミーの形がルールに合っていません。その場合は、別のルールに合う形のダミー値で試し直してください。
その後、Claude Codeに「ダミー値を入れた設定ファイルを書いて」と頼み、PreToolUseのブロックが働くことを確認します。
作業ツリーを手動で点検する
hookとpre-commitは新しい変更を見張る仕組みです。いま手元にあるファイルの状態を確かめたいときは、dirモードを使います。対象を省くとカレントディレクトリが走査されます。
gitleaks dir -v --redact .Claude Codeに一括修正を任せた後や、新しいブランチを取り込んだ後に流すと、hookを通らなかった変更の取りこぼしを拾えます。編集のたびに検査をhookへ寄せる考え方は、ShellCheckのhookと同じです。
誤検知と既存の履歴をどう扱うか
導入直後は、テスト用の鍵やサンプルが引っかかります。除外の手段は、粒度の順に4つあります。
| 手段 | 書く場所 | 使いどころ |
|---|---|---|
gitleaks:allowコメント | 書く場所該当行の末尾 | 使いどころ意図的に置いたテスト用の値1つ |
.gitleaksignore | 書く場所リポジトリのルート | 使いどころレポートのFingerprintで個別の検出を除外 |
--baseline-path | 書く場所実行時のオプション | 使いどころ既存の履歴にある検出をまとめて棚上げ |
.gitleaks.toml | 書く場所リポジトリのルート | 使いどころルールの無効化やallowlistの追加 |
行末コメントは次の形です。
discord_client_secret = "8dyfuiRyq=vVc3RRr_edRk-fK__JItpZ" #gitleaks:allow既存のリポジトリに入れるときは、最初にベースラインを作ります。READMEの手順では、まずレポートを保存し、次回から--baseline-pathで指定します。
gitleaks git --report-path gitleaks-report.json
gitleaks git --baseline-path gitleaks-report.json --report-path findings.json2回目のfindings.jsonには、ベースラインに無い新しい検出だけが残ります。
設定ファイルは、--config、環境変数GITLEAKS_CONFIG、環境変数GITLEAKS_CONFIG_TOML、対象パスの.gitleaks.tomlの順に探されます。どれも無ければ組み込みの既定設定が使われます。既定を残して一部を調整するなら、次のようにextendします。
title = "project gitleaks config"
[extend]
useDefault = true
disabledRules = ["generic-api-key"]generic-api-keyを切ると誤検知は減りますが、その分の検出も失います。READMEの例にもあるルールですが、外すかどうかは実際の誤検知の量を見て決めてください。
この構成が止められないもの
二段の関門でも、次の抜け道は残ります。
- Bashで作られたファイル:
echo "KEY=..." > config.envのように、WriteやEditを通らない書き込みはPreToolUseで検査されません。最後の関門のpre-commitが受け持ちます git commit -a:-aはコミットの瞬間にステージするため、hookの時点の--stagedには変更が入っていません。こちらもpre-commitで止まりますgit commit --no-verify: pre-commitを飛ばせます。上のスクリプトはこの文字列を含むコマンドを拒否しますが、ほかの経路で飛ばされる可能性は残ります- すでに履歴にある値: 新規の混入しか止められません。過去分は
gitleaks gitで棚卸しし、見つかった鍵は無効化して再発行するのが前提です
.envそのものをClaudeに読ませない対策は別の層です。Read denyの書き方と組み合わせると、入口の読み取りと出口の書き込みの両方を塞げます。
コミット時の検査という意味では、メッセージの規約を検査するcommitlintの設定と同じ位置にpre-commitの検査を足す形になります。
まとめ
gitleaksの検出結果を、Claude Codeのhookでブロックに変換する部分が、この構成の要です。終了コード1をそのまま返すと素通りし、onFailureを付けないと設定ミスも素通りします。
最初はgitleaks git --stagedのpre-commitだけを入れ、誤検知の除外を固めてから、PreToolUse hookを足す順番が無理のない進め方です。導入先がモノレポや古いリポジトリなら、ベースラインを先に作ってから始めてください。