結論
-
.gitignoreに足してgit rm --cached .envしても、1つ前のコミットからはgit show HEAD~1:.envで中身が出ました(筆者の環境で確認)。 - 履歴に残っているかは
git log --all --full-history -- '.env*' '**/.env*'などで探せます。消すときはgit filter-repo --sensitive-data-removal --invert-paths --path .envを、新しく取ったクローンで実行します(GitHub公式の手順)。 -
履歴を書き換えても、漏れた鍵は無効になりません。最初にやるのは鍵の再発行(ローテーション)です(GitHub公式)。書き換え前のクローンには、消したはずの
.envがそのまま残っていました(筆者の環境で確認)。
この記事は「AIエージェントと秘密情報」の検証シリーズです。新しい検証は X(@supportapp3_lab)でもお知らせしています。
この記事は、AIエージェントと秘密情報の話の続きです。Codex CLIに.envを読ませない設定 では、.env の読み取りを deny しても git show HEAD:.env で中身が出ることを確かめました。deny は「いま作業ツリーにあるファイル」を守る設定で、git の中に残った同じ内容までは守りません。今回はその穴を、履歴の側から塞ぎます。
検証環境と範囲
- Debian 13 / git 2.47.3 / git-filter-repo 2.47.0(
pipで一時的な venv に導入) - GitHub ではなく、ローカルの bare リポジトリを
originに見立てて確かめました。 - 置いた値はすべてダミーです(
DUMMY_API_KEY=dummy_not_a_secret_123など)。検証後に削除しました。
確かめていないこと
- GitHub 上での挙動(キャッシュされた表示、PR 参照、フォーク)。この部分は GitHub 公式ドキュメントの記載だけを紹介します。
- macOS と Windows での挙動。
- Claude Code で
Read(./.env)を deny したときにgit show HEAD~1:.envが止まるか(後述)。
1. 「消したつもり」の状態を作る
.env と config/.env.local をコミットしてから、よくある手順で「管理対象から外し」ました。
git add . && git commit -m "initial commit" # うっかり .env ごとコミット
git rm --cached .env config/.env.local
printf '.env\n.env.*\n' > .gitignore
git add .gitignore && git commit -m "stop tracking .env"
この状態で試した結果です。
$ git status --short --ignored
!! .env
!! config/
$ git show HEAD:.env
fatal: path '.env' exists on disk, but not in 'HEAD'
$ git show HEAD~1:.env
DUMMY_API_KEY=dummy_not_a_secret_123
最新のコミットには .env がありません。でも1つ前のコミットを指定すると、中身がそのまま出ました。push 済みなら、リモートにも同じ履歴があります。「最新のコミットに無い」は「安全」ではありません。
2. 自分のリポジトリを点検する3つのコマンド
① .env を追加・削除したコミットを探す
$ git log --all --full-history --oneline -- '.env*' '**/.env*'
74d5f5a stop tracking .env
03f2157 initial commit
--all で全ブランチとタグ、--full-history で履歴の簡略化をせずに探します。何も出なければ、その名前のファイルはコミットされていません。
② 履歴の中にある .env 系のファイルを一覧にする
$ git rev-list --all --objects | grep -E '(^| |/)\.env'
17f709d7339578e5b4b54e9d4153693ddf609837 .env
7de58cddc49b61f742d30ac9d03fb9fb8e9cff12 config/.env.local
左の値はファイルの中身(blob)のハッシュです。あとで「本当に消えたか」を確かめるときに使います。
③ 値そのものを、全コミットから探す
$ git grep -n dummy_ $(git rev-list --all)
03f2157484012a8b12d24cd07642ed29f810b0c7:.env:1:DUMMY_API_KEY=dummy_not_a_secret_123
03f2157484012a8b12d24cd07642ed29f810b0c7:config/.env.local:1:DUMMY_DB_PASS=dummy_local_456
ファイル名を変えてコミットしてしまった場合も、これなら見つかります。本物の鍵を探すときは、鍵の値そのものをコマンドに打たないでください。シェルの履歴に残ります。API_KEY= のような変数名で探すのがおすすめです(筆者の提案)。
おまけ:stash にも残る
git stash -u は ignored のファイルを含みません。筆者の環境では、変更が .env だけだったので stash 自体が作られませんでした。一方、git stash --all は ignored のファイルも含めるので、.env が stash に入りました。
$ git stash -u
No local changes to save
$ git stash --all
Saved working directory and index state WIP on main: 74d5f5a stop tracking .env
$ git show 'stash@{0}^3:.env'
DUMMY_API_KEY=dummy_changed_789
stash はローカルにしか残らず、普通の push では送られません。ただ、AIエージェントが git show で読める場所が1つ増えます。
3. 見つかったら:やる順番
① 先に鍵を再発行する
GitHub の公式ドキュメントは、秘密情報なら最初に revoke や rotate をするよう書いています。再発行すれば古い鍵はもう使えないので、それで十分なら、履歴の書き換えまではしなくてよい場合もあるとも書かれています。
履歴を書き換えても、それまでに誰かが取得した鍵は使えるままです。順番を逆にしないでください。
② 新しく取ったクローンで git-filter-repo を実行する
--sensitive-data-removal は git-filter-repo 2.47 以上で使えます(GitHub公式)。
最初に、作業していたリポジトリでそのまま実行してみました。すると、こう言われて止まりました。
$ git filter-repo --sensitive-data-removal --invert-paths --path .env --path config/.env.local
Aborting: Refusing to destructively overwrite repo history since
this does not look like a fresh clone.
(expected at most one entry in the reflog for HEAD)
Note: when cloning local repositories, you need to pass
--no-local to git clone to avoid this issue.
Please operate on a fresh clone instead. If you want to proceed
anyway, use --force.
GitHub の手順も、まずクローンしてからその中で実行する流れです。新しくクローンして実行し直しました(ローカルのリポジトリからクローンしたので --no-local を付けています)。
$ git clone -q --no-local origin.git fresh
$ cd fresh
$ git filter-repo --sensitive-data-removal --invert-paths --path .env --path config/.env.local
NOTICE: Fetching all refs from origin to make sure we rewrite
all history that may reference the sensitive data, via
git fetch -q --prune --update-head-ok --refmap "" origin +refs/*:refs/*
Parsed 2 commitsHEAD is now at db361b5 stop tracking .env
New history written in 0.03 seconds; now repacking/cleaning...
You rewrote 2 (of 2) commits.
NOTE: First Changed Commit(s) is/are:
03f2157484012a8b12d24cd07642ed29f810b0c7
NOTE: LFS object orphaning not checked (LFS not in use)
(以下、次の手順の案内が続く)
-
--pathには、ファイル名だけではなくリポジトリ内のパスを書きます。移動や改名をしたことがあるなら、前のパスも--pathで足してください(GitHub公式)。 -
First Changed Commit(s)の値は、GitHub Support に連絡するときに使います(GitHub公式)。
③ 消えたか確かめる
$ git log --all --full-history --oneline -- '.env*' '**/.env*'
$ git grep -n dummy_ $(git rev-list --all)
$ git cat-file -p 17f709d7339578e5b4b54e9d4153693ddf609837
fatal: Not a valid object name 17f709d7339578e5b4b54e9d4153693ddf609837
$ git log --oneline
db361b5 stop tracking .env
9278ca3 initial commit
探すコマンドは2つとも何も出さなくなり、2章②で控えた blob のハッシュも見つからなくなりました。コミットのハッシュは全部変わっています。
--sensitive-data-removal を付けた今回は、origin の設定が残りました(git remote -v で確認)。別の新しいクローンで、このオプションを付けずに実行したときは origin の設定が消えました。次の push の前に git remote -v を見ておくと安全です(筆者の環境で確認)。
④ force push する
$ git push --force --mirror origin
To /path/to/origin.git
+ 74d5f5a...db361b5 main -> main (forced update)
(パスは書き換えています)
GitHub では、refs/pull/ で始まる参照は読み取り専用なので、push が失敗します(GitHub公式)。ブランチ保護で force push を禁止しているなら、一時的に外す必要があります。
4. 消しても残る場所
① 書き換え前のクローン(実機で確認)
書き換え前に作っておいた別のクローンでは、旧 blob も旧コミットもそのまま読めました。
$ git cat-file -p 17f709d7339578e5b4b54e9d4153693ddf609837
DUMMY_API_KEY=dummy_not_a_secret_123
$ git show HEAD~1:.env
DUMMY_API_KEY=dummy_not_a_secret_123
GitHub 公式によると、古いクローンを持つ人が書き換え後に git pull して git push すると、秘密情報がまた戻ってきます。古いクローンは捨ててクローンし直してもらうのが確実です。
② リモート側にも、しばらく残る(実機で確認)
force push した直後の bare リポジトリでは、旧 blob がまだ残っていました。git gc --prune=now を実行して、やっと消えました。
$ git --git-dir=origin.git cat-file -t 17f709d7339578e5b4b54e9d4153693ddf609837
blob
$ git --git-dir=origin.git gc -q --prune=now
$ git --git-dir=origin.git cat-file -t 17f709d7339578e5b4b54e9d4153693ddf609837
fatal: git cat-file: could not get object info
これは自分で管理する bare リポジトリでの結果です。GitHub ではサーバー側の gc を自分では実行できません。
③ GitHub 上(公式の記載のみ。実機では未確認)
GitHub 公式によると、書き換えて force push したあとも、次の場所からは読めます。
- クローンとフォーク
- SHA を直接指定した、GitHub 上のキャッシュされた表示
- そのコミットを参照している PR
キャッシュされた表示と PR の参照は、GitHub Support に頼めば消してもらえます。ただし、鍵のローテーションでリスクを抑えられる場合は対応しないと明記されています。フォークに残ったものは、フォークの持ち主に頼むしかありません。
5. AIエージェントの deny では止まらない理由
Claude Code の公式ドキュメントでは、Read の deny が効く範囲を次のように説明しています。
- 組み込みのファイルツールと、Bash で認識できるファイルコマンド(
cat、head、tail、sed、tee)、リダイレクトの対象には効く。 - ファイル名を指定せずに読むコマンドや、スクリプトのように間接的にファイルを読むサブプロセスには効かない。OS レベルで止めたいなら sandbox を使う。
-
gitの読み取り系のコマンドは、確認なしで実行される組み込みの読み取り専用コマンドに含まれる。
git show HEAD~1:.env は、作業ツリーの .env ではなく .git の中のオブジェクトを読みます。なので Claude Code で Read(./.env) を deny していても、止まらない可能性があります。ただし、これは公式の記載から考えた推測で、実機では確かめていません。 Codex CLI では、deny を設定しても git show HEAD:.env で読めたことを 前回の記事 で確認しています。
AI に読ませたくない鍵が履歴に一度でも入ったなら、deny の設定より先に、鍵の再発行が必要です。
6. 再発防止
GitHub 公式が挙げている方法です。
-
.gitignoreに.envを書き、そのファイル自体もコミットして共有する -
git add .やgit commit -aを避け、ファイルを1つずつ追加する。git diff --cachedで中身を見てからコミットする - pre-commit フックで秘密情報を検出する(git-secrets や gitleaks など)
- GitHub の push protection を有効にする
AIエージェントの側では、次の設定も合わせて行ってください。
- Claude Code: settings.json の permissions.deny
- Codex CLI: permission profile の deny
- どのエージェントが
.envを読めそうかの点検: ファイル名だけで点検する日本語CLI
まとめ
-
git rm --cachedと.gitignoreだけでは、過去のコミットに.envが残る(筆者の環境で確認)。 -
git log --all --full-history、git rev-list --all --objects、全コミットへのgit grepで探せる。 - 見つかったら、まず鍵を再発行する。そのうえで必要なら、新しく取ったクローンで
git filter-repo --sensitive-data-removalを実行し、force push する。古いクローン、フォーク、GitHub 上のキャッシュには残る(クローンは実機、それ以外は公式)。
関連記事:
- Codex CLIに.envを読ませない設定:公式例の "**/*.env" だけでは .env.local と深い階層が読めた
- Claude Codeに.envを読ませない設定は settings.json の permissions.deny に書く
- Claude Codeの会話記録は ~/.claude/projects/ に平文で30日残る
- AIエージェントに.envを読まれていないか、ファイル名だけで点検する日本語CLIを作った
参考(公式):
- Removing sensitive data from a repository – GitHub Docs
- git-filter-repo(newren/git-filter-repo)
- git-stash ドキュメント
- Configure permissions – Claude Code Docs
※ 仕様は 2026-10-19 時点の公式ドキュメントに基づきます。検証は git 2.47.3 / git-filter-repo 2.47.0 で、Linux の使い捨て環境でのみ行いました。
試してみる
この記事の①の点検をまとめて行うCLIを公開しました(MIT License): https://github.com/supportapp3-lab/ai-secret-check
npx github:supportapp3-lab/ai-secret-check scan-history ./調べたいリポジトリ
全ブランチ・stash を含む履歴から、どの階層の .env / .env.* が追加されたかを、コミットの短いハッシュと件数だけ表示します。ファイルの中身は取り出しません。見つかったら、まず鍵の再発行から始めてください。
更新情報やちょっとしたTipsは X(@supportapp3_lab)で発信しています。