Claude Media
Claude Codeでgitleaksを使う — 秘密情報のコミットをhookで止める

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: gitleaks
pre-commit install

フレームワークを入れたくない場合は、.git/hooks/pre-commitに同じコマンドを置けば足ります。

#!/bin/sh
exec gitleaks git --pre-commit --redact --staged --verbose

chmod +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つあります。

  1. gitleaksが検出すると終了コード1で終わりますが、Claude Codeではexit 0と2以外は「非ブロックのエラー」で、アクションが進んでしまいます。block関数で明示的にexit 2へ変換しています。
  2. 検出結果は--redact付きで標準エラーに流します。Claudeに返る文面に秘密情報そのものを載せないためです。
  3. 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での扱い
0PreToolUseでの扱い成功。ツールは実行される
2PreToolUseでの扱いブロック。標準エラーが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.json

2回目の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を足す順番が無理のない進め方です。導入先がモノレポや古いリポジトリなら、ベースラインを先に作ってから始めてください。

この記事を共有:XはてブLinkedIn