AI コーディングで secret を漏らさないための4層防御
顧客向けマニュアルにパスワードを直書きしてコミットし、後で履歴抹消とパスワード全ローテーションをやるハメになりました。同じミスをしないための4層防御を共有します。AI時代こそ、git レベルの決定論的な防御が効きます。
何が起きたか
顧客向けの運用マニュアルを書いていました。サーバーへのログイン情報、各種管理画面の URL、メール設定。引き継ぎや保守でクライアント側の担当者が見るための、いわゆる「手順書」です。
その中に、ユーザーID や パスワードを直書きし、そのままコミットしてしまいました…。今考えるとなんでこんなことをしてしまったのか分かりません。不注意だったのか疲れて頭が回ってなかったのか。
すぐに気づいて対応できれば良かったのですが、気づかずに、その後も作業を続けてしまいました。他の機能を実装し、ドキュメントを追加し、リファクタしてコミットを重ね、気がつくと十数コミットが上に積み重なっていました。
そして、ふと気付きます。
「パスワード入りのファイルをコミットし、Github に公開してしまってるやん…。」
やってしまったなと思いましたが、状況を受け入れて対応するしかありません。
ここからの作業の非生産的なこと…(実作業をしたのは Claude Code ですが)。
-
git filter-repoで履歴から該当ファイルを除去 - 該当するパスワード・API キーを全部ローテーション
- リモートに force push
- GitHub 側のキャッシュやフォークの残り問題を確認
幸い public リポジトリではなかったので致命傷は避けられましたが、無駄な時間と本来不要だったトークン使用が発生しました。
なぜ起きたか
「自分が雑だった」のは間違いないです。しかし、今後やらかさないという自信はない。構造的な理由があります。
AIコーディングで書くスピードが上がっている
ドキュメントの初稿が5分で出る。実装と並行してマニュアルも書ける。便利なんですが、人間側のレビュー帯域はそのまま。AI に書かせる量が増えるほど、自分が「全行を厳密に見ているか」と問われると、正直怪しい。
書いた瞬間に「これはバージョン履歴に残したらまずいやつ」とは思うんです。でも次の AI とのやりとりが始まると、そのタスクは記憶の表層から滑り落ちる。AI コーディングのテンポは速く、一つ前のターンで何を保留にしたか、平気で忘れます。
つまり「"気をつける" は枯渇する資源」なんです。締切前、疲労時、深夜、AI に乗せられて勢いがついている時にこそ、こういうミスは起きる。「次は気をつけよう」は何の対策にもなりません。
AI でなく、仕組みに守らせる
最初、Claude Code のフックで秘密情報を検知して止めればいいと考えましたが、Claude Code 経由でコミットしない場合は当然発火しないので、自分でエディタに直接貼り付けたりターミナル直でコマンドを打つ場合には対応できません。別の AI ツールで書いた場合も当然対象外です。
一方、git はすべての入口を一本化するポイント です。誰が・何で書いたとしても、「コミット」という関門は必ず通る。だから防御はここに置くのが構造的に理にかなっています。
4 層防御の全体像

役割を明確に分けるのがポイントです。
| 層 | 役割 | 担い手 | 強さ |
|---|---|---|---|
| 1. 設計層 | そもそも秘密を書く場所を分ける | 人間(規約) | ◎ |
| 2. git pre-commit 層 | コミット時に決定論的スキャン | gitleaks | ◎ |
| 3. push protection 層 | リモート到達前に GitHub が止める | GitHub | ○ |
| 4. Claude Code hook 層 | AI が書く瞬間に止める | Claude Code | △(補助) |
主防御は 1 と 2。3 はフェイルセーフ。4 は補助。この順序が肝です。
1. 設計層 ─ 仕組みで「混ざりようがない」状態を作る
最強でコスト最低の層です。実値を含むファイルそのものを、git の追跡対象から外す。
一番シンプルなのは、機密を含むマニュアルやメモを _credentials/ や *.local.md のような場所に置き、.gitignore で除外することです。
_credentials/
*.local.md
*.secret.md
credentials*.md
**/draft/
こうすれば、マニュアル本文には実値を堂々と書いて構いません。「ID は xxx、PW は yyy」とそのまま書ける。読み手にとってわかりやすい形を保ったまま、git 履歴には何も残らない。「気をつける」じゃなく「混ざりようがない構造」を作るのが本質です。
失われるのは git によるバージョン管理だけです。継続的に更新する案件で履歴を残したい場合は、1Password / Bitwarden の共有 vault、Notion や Confluence の権限付きページなど、git の外で履歴を持ってくれる場所に置けば代替できます。
あなたが何度気をつけても、人間の注意力は枯渇します。だから設計で先に潰す。これが第一層。
2. git pre-commit 層 ─ gitleaks を入れる
ここが本命の自動化です。使うツールは 2 つ。
-
gitleaks ── git リポジトリやファイルから secret を検出する OSS。Go の単一バイナリで速く、AWS キーや GitHub Token などの汎用パターンに加えて、
.gitleaks.tomlでプロジェクト固有のルールも追加できます。secret 検知系のデファクト -
lefthook ── git hooks を YAML で宣言的に管理する OSS。
pre-commit/pre-pushなどをlefthook.yml1ファイルで集中管理でき、複数チェックの並列実行やSKIP=<name> git commit ...での一時バイパスもできます
gitleaks は「何を検知するか」、lefthook は「いつ走らせるか」を担当する役割分担です。
導入は 1 行。
brew install gitleaks lefthook
lefthook でフックに結線します。リポジトリのルート に lefthook.yml を置きます。
pre-commit:
commands:
gitleaks:
run: gitleaks git --staged --redact --verbose
配置後、初回だけ lefthook install を実行すると .git/hooks/pre-commit に gateway が仕込まれます。
lefthook install
git clone した直後のチームメンバーも各自 lefthook install が必要なので、README や package.json の postinstall などに書いておくと事故が減ります。
これだけで、commit するたびに staged されたファイルがスキャンされ、AWS キーや GitHub Token などの汎用パターンは止まります。
補足: gitleaks v8.19.0 で従来の
protect/detectサブコマンドは deprecated になり、現行はgit/dir/stdinの 3 コマンドに整理されています。旧コマンドも後方互換で動きますが--helpから隠されているので、新規導入なら新コマンドで書く方が安全です。
プロジェクト固有の値(独自の API キー名、パスワード: のような日本語フィールド記法など)は .gitleaks.toml でカスタムルールを足します。最小構成のダミーで雰囲気を示すとこんな感じです。
# プロジェクト固有のカスタムルール
[[rules]]
id = "japanese-password-field"
description = "「パスワード: xxx」「PW: xxx」のような日本語フィールド経由の実値検知"
regex = '''(?i)(パスワード|password|pw|pass)\s*[::=]\s*["']?[A-Za-z0-9!@#$%^&*\-_=+]{6,}["']?'''
tags = ["password", "japanese"]
[[rules]]
id = "service-api-key"
description = "自社サービス独自の API キー(プレフィックスで識別)"
regex = '''mycompany_(live|test)_[A-Za-z0-9]{24,}'''
tags = ["api-key"]
# テンプレートやダミー値は検知から除外(誤検知ノイズ対策)
[[allowlists]]
paths = [
'''docs/.*\.template\.md''',
]
regexes = [
'''DUMMY_PASSWORD|<your-password-here>|xxxxxxxx''',
]
ポイントは「汎用ルール(API キー形式)+ プロジェクト固有ルール(フィールド名 + 値)」の両建てにすること。特に日本語の運用マニュアルは「パスワード:」のような書き方が多いので、フィールド名ベースのルールが効きます。さらに [[allowlists]] でテンプレートやダミー値を除外しておくと、運用が回り出した後の誤検知ノイズで疲弊することがありません。
正規表現の細かい構文や利用できるディレクティブは gitleaks の Configuration ドキュメント を参照してください。
3. push protection 層 ─ GitHub 側で最後の砦を作る
ここまでやっても、git commit --no-verify で自分が hook を抜けたら意味がありません。そのフェイルセーフが GitHub の Push Protection です。
リポジトリ設定で有効化します(GitHub の UI 改修で「Advanced Security」配下に整理されました)。
Settings → Advanced Security → Secret Protection を Enable → Push protection を Enable
これを ON にすると、Push の段階で GitHub 側が既知パターン(AWS、Stripe、OpenAI、Anthropic など主要サービスの API キー形式)を検知して止めます。検知対象パターンの一覧は Supported secret scanning patterns で確認できます。
ここで 1 点だけ注意。public リポジトリは無料で自動的に有効になりますが、private リポジトリで使うには GitHub Team 以上 + Secret Protection の有効化(GitHub Advanced Security の一部、有料)が必要です。クライアントワークの private リポジトリで効かせたい場合は、組織の課金プランを確認してから有効化してください。詳細は About secret scanning を参照。
--no-verify を抜けても、別マシンから push しても、サーバー側で止まる。これが「最後の砦」です。public で運用するプロジェクトなら入れない理由がありません。
4. Claude Code hook 層 ─ 補助として置く
ここまで来てようやく、Claude Code 側のフックの話です。順序が逆だと脆くなるので、最後に置きました。
~/.claude/settings.json で PreToolUse に gitleaks を呼ぶフックを設定すると、Claude が Write / Edit を実行する瞬間に内容をスキャンできます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/secret-guard.sh" }
]
}
]
}
}
呼び出される ~/.claude/hooks/secret-guard.sh の最小実装はこんな感じです(依存: gitleaks, jq)。
#!/usr/bin/env bash
# ~/.claude/hooks/secret-guard.sh
set -euo pipefail
# Claude Code は PreToolUse フックに JSON を stdin で渡す
PAYLOAD=$(cat)
# Write の content、または Edit の new_string を取り出す
CONTENT=$(printf '%s' "$PAYLOAD" | jq -r '
.tool_input.content //
.tool_input.new_string //
empty
')
# 空なら検査不要
[[ -z "$CONTENT" ]] && exit 0
# 一時ファイルに書き出して gitleaks に渡す(v8.19.0 以降は dir コマンド)
TMP=$(mktemp -t secret-guard.XXXXXX)
trap 'rm -f "$TMP"' EXIT
printf '%s' "$CONTENT" > "$TMP"
if gitleaks dir "$TMP" --no-banner --redact >/dev/null 2>&1; then
exit 0 # 検知なし → そのまま許可
fi
# 検知あり → exit 2 でブロック。stderr の内容は Claude にフィードバックされる
cat >&2 <<'EOF'
secret-guard: secret らしき文字列を検知したため Write/Edit をブロックしました。
- 機密値は .gitignore された場所 (_credentials/ など) に分離してください
- テンプレートには <PLACEHOLDER> など明示的なダミーを使ってください
- 誤検知なら .gitleaks.toml の [[allowlists]] に追加してください
EOF
exit 2
実行権限を付けるのを忘れずに。
chmod +x ~/.claude/hooks/secret-guard.sh
仕組みは単純で、Claude が Write/Edit/MultiEdit を実行する直前にこのスクリプトが呼ばれ、書き込もうとしている文字列を gitleaks に通すだけ。検知したら exit 2 で Claude にブロックを返し、stderr に書いた文章はそのまま Claude へのフィードバックとして渡るので、AI 側にも「なぜ止まったか」が伝わって自己修正に入ります。
PreToolUse 以外のフックポイントや JSON スキーマの詳細は Claude Code Hooks Reference を参照してください。
ただしこれを主防御にしないでください。理由は3つあります。
- Claude が書いた瞬間しか効かない ─ 手書き、他の AI、シェルからの追記、全部素通り
- 判断が確率的 ─ AI に検知させると false negative/positive が出る。決定論的な機械の方が信頼できる
- 責任の所在が曖昧になる ─ 「AI が見てるから大丈夫」という運用は人間の注意を弱める。最終防衛線を AI に任せると組織として脆い
補助としては有効です。あなたが Claude Code を主な書き手にしているなら、入れる価値はあります。でも、層 1〜3 を入れずにここだけ入れるのは順序が逆です。
もし、もう漏らした後だったら
事後対応は順序が肝心です。最優先はローテーション、履歴削除はそのあと。ここを逆にすると、復旧作業をしている間もシークレットが生きたまま放置されます。
1. まず必ずパスワードをローテーション
これが鉄則であり、最初にやることです。一度 push したシークレットは「漏れたもの」として扱う。履歴を消しても、その間にクローンした人、フォーク、GitHub の内部キャッシュ、検索インデックス、どこに残っているか分かりません。
なぜ削除より先かというと、履歴の書き換えは時間がかかるうえ失敗もしうる作業で、やっている間もシークレットは有効なままだからです。先に値を切り替えてしまえば、クリーンアップにどれだけ手間取っても、漏れたシークレットはただの無効な文字列になる。GitHub 公式も「機密データがシークレットなら、最初のステップとして revoke / rotate せよ。それで問題が解決することも多い」と明言しています。
履歴から消すのは「これ以上の露出を防ぐ」ためであって、「無かったことにする」ためではない。該当する API キー・パスワードは全部新しい値に切り替えてください。これをやらないと後続の作業の意味が半分以下になります。
2. 履歴から削除
ローテーションで封じ込めたうえで、履歴も掃除します。git filter-branch は非推奨。今は git filter-repo または BFG Repo-Cleaner を使います。
git filter-repo --path docs/secret.md --invert-paths
git push origin --force --all
3. キャッシュ問題を理解する
GitHub は force push 後も一定期間データを保持します。フォークがあるとそこにも残る。public リポジトリで漏らした場合、GitHub Support に依頼してキャッシュを消してもらう手順もあります。詳細は GitHub のセンシティブデータ削除ガイド を参照してください。
まとめ
- AI コーディングで速度が上がるほど、人間の注意力は相対的に弱くなる
- だから「気をつける」では防げない。仕組みで止める
- 主防御は git レベル(pre-commit hook + push protection)
- Claude Code フックは補助。これを主防御にすると脆い
- 「機械が決定論的に止める、AI と人間はその上で自由に動く」が AI 時代の防御の形
ちょっとの手間で以降の事故が止まるなら、導入しない理由はありません。
Discussion
先にローテーションですね。
痕跡削除はそのあと。
よくまとまっていて、自分でも書いているのに
最後の詰めでミスりましたね。
ご指摘ありがとうございます!おっしゃるとおりで誤った順序で書いていました。該当箇所を修正しました。助かりました、感謝🙏