APIキーやパスワードをGitHubに送ってしまったら、最初にやるのは履歴の削除ではありません。鍵を止めて作り直すことです。APIキーは、外部のサービスを動かすための合言葉のような文字列で、手に入れた人はそのサービスを使えます。履歴を消す作業には時間がかかり、消しても他の人の手元に残ります。止めてしまえば、残っていても使えません。
読み終わると、何をどの順番でやるか、どこまで消せてどこから消せないかが分かり、次に同じことが起きない形まで作れます。
この記事の内容
先に答えると
順番は、鍵を止める、使われた形跡を見る、履歴から消す、関係者に知らせる、再発を防ぐ、です。GitHub公式の手順も、履歴を書き換える前に「revoke and/or rotate」(無効化か作り直し)を求めています。今すぐやるのは1番目だけです。
- 鍵を消すコミットをしても、過去のコミットに鍵は残ります
- 公開リポジトリなら、すでに誰かに取られた前提で動きます
- 履歴から消すのは
git filter-repoです。上書きのpushと、GitHubサポートへの連絡まで要ることがあります - 他の人のクローン(手元の複製)とフォーク(GitHub上の複製)に残った分は、こちらからは消せません
- Freeの非公開リポジトリではGitHubの検出機能が使えないので、手元で止める道具を入れます
順番の全体
| 順番 | やること | 具体的には | 急ぎ |
|---|---|---|---|
| ① | 鍵を止める | 発行元の管理画面で無効化し、新しい鍵を作って差し替える | 今すぐ |
| ② | 使われた形跡を見る | 発行元の利用履歴・請求額、GitHubのSecurity log | 今日中 |
| ③ | 履歴から消す | git filter-repo、force push、必要ならサポートへ連絡 | 落ち着いてから |
| ④ | 他の人に知らせる | クローンを消して取り直してもらう | ③と同時 |
| ⑤ | 再発を防ぐ | 鍵の置き場所、送る前の検査 | 今週中 |

①鍵を止める
サンプル料理教室の代表が、予約メールのスクリプト send_mail.py にメール配信サービスの鍵を書いたまま、リポジトリ reservation-mail に送ってしまったとします。リポジトリは、ファイルと履歴をまとめて入れておく保管場所です。
- メール配信サービスの管理画面を開きます。
- 新しい鍵を作ります。
- サーバーの
.env(鍵を書いておく設定ファイル)を新しい鍵に差し替えます。 - 予約メールが届くかを確かめます。
- 古い鍵を無効化します。
これは止まる時間を短くしたい順番です。公開リポジトリに出てしまったなら、予約メールが少し止まっても先に古い鍵を止めます。何分で拾われるかの数字はGitHubの公式資料にはありません。ただ、公開リポジトリの中身は誰でも見られるので、自動で集めるプログラムが巡回している前提で動きます。
GitHubのトークン(パスワードの代わりに使う文字列)については、有効なものを公開リポジトリに送ると、GitHubが自動で無効にします。外部サービスの鍵まで止めてくれるとは考えず、自分で止めます。非公開リポジトリでも止めます。招待した人全員とそのクローンに鍵が渡っているからです。
②使われた形跡を見る
見る場所は、まず鍵の発行元です。メール配信サービスなら送信履歴と請求額、クラウドなら利用履歴です。身に覚えのない送信や請求があれば、発行元のサポートに連絡します。
GitHub自身のアカウントが心配なときは、Security logを見ます。
- 右上のプロフィール写真から「Settings」を開きます。
- サイドバーの「Archives」にある「Security log」を開きます。
- 身に覚えのない操作がないかを見ます。直近90日分の操作、実行した人、国、日時が並びます。

消すコミットでは消えない
ここからは練習用のフォルダでGit(ファイルの変更履歴を残す仕組み)を動かします。リモート(送り先)もGitHubではなく同じフォルダの中に作りました。鍵は xxxxxxxx-fake-key という偽物です。鍵を直接書いた send_mail.py をコミットして送ったあと、気づいて鍵を消しました。コミットは、変更を履歴に1回分記録する操作です。
$ git commit -m "予約メールのスクリプトを追加"
[main (root-commit) 3cf1bfe] 予約メールのスクリプトを追加
1 file changed, 2 insertions(+)
create mode 100644 send_mail.py
$ git push -u origin main
To ../remote.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
$ cat send_mail.py
import os
MAIL_API_KEY = os.environ["MAIL_API_KEY"]
print("send")
$ git commit -m "鍵をコードから消す"
[main 3e5c9e0] 鍵をコードから消す
1 file changed, 2 insertions(+), 1 deletion(-)
$ git push
To ../remote.git
3cf1bfe..3e5c9e0 main -> main
いまのファイルを検索すると、鍵は見つかりません。
$ git grep fake-key
何も出ないので消えたように見えます。履歴を見ます。
$ git log -p -- send_mail.py
commit 3e5c9e0e7b873983c06fcb7e7eda415d669f4e77
Author: 代表 <daihyo@sample-cooking.example>
Date: Sat Nov 7 09:25:16 2026 +0900
鍵をコードから消す
diff --git a/send_mail.py b/send_mail.py
index e541460..37c5f8e 100644
--- a/send_mail.py
+++ b/send_mail.py
@@ -1,2 +1,3 @@
-MAIL_API_KEY = "xxxxxxxx-fake-key"
+import os
+MAIL_API_KEY = os.environ["MAIL_API_KEY"]
print("send")
commit 3cf1bfe691057f771535f8f7592f55155d65ed68
Author: 代表 <daihyo@sample-cooking.example>
Date: Sat Nov 7 09:25:16 2026 +0900
予約メールのスクリプトを追加
diff --git a/send_mail.py b/send_mail.py
new file mode 100644
index 0000000..e541460
--- /dev/null
+++ b/send_mail.py
@@ -0,0 +1,2 @@
+MAIL_API_KEY = "xxxxxxxx-fake-key"
+print("send")
鍵はそのまま読めます。消すコミットも「この行を消した」という記録なので、消した行を載せています。git revert(打ち消すコミットを足す操作)で戻しても同じです。
どのコミットに鍵が入っているかは git log -S で探せます。あとからクローンした人の手元でも、同じものが見えます。
$ git log -S fake-key --oneline
3e5c9e0 鍵をコードから消す
3cf1bfe 予約メールのスクリプトを追加
$ git clone -q ../remote.git ../reservation-mail-b
$ git -C ../reservation-mail-b log -S fake-key --oneline
3e5c9e0 鍵をコードから消す
3cf1bfe 予約メールのスクリプトを追加
$ git -C ../reservation-mail-b show HEAD~1:send_mail.py
MAIL_API_KEY = "xxxxxxxx-fake-key"
print("send")

pushする前に気づいたとき
まだ送っていないなら話は簡単です。鍵は手元から出ていないので、止める必要もありません。直前のコミットを作り直します。次の例は、料金表の修正に git add . で .env まで混ざった場合です。
$ git status -sb
## main...origin/main [ahead 1]
$ git log origin/main..HEAD --oneline
b386b6f 料金表を修正
$ git show --stat --format=%h HEAD
b386b6f
.env | 1 +
menu.html | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
ahead 1 は、送っていないコミットが1つあるという印です。.env を外し、.gitignore(Gitに入れないファイルの一覧)を足してコミットをやり直します。
$ git rm --cached .env
rm '.env'
$ echo '.env' > .gitignore
$ git add .gitignore
$ git commit --amend --no-edit
[main fe140d7] 料金表を修正
Date: Sat Nov 7 09:25:16 2026 +0900
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 .gitignore
$ git log -S fake-key --oneline
$ git push
To ../remote.git
edcb6a5..fe140d7 main -> main
--amend は、直前のコミットを作り直す操作です。GitHubの大きなファイルの案内にも、同じ手順があります。
pushしたあとに履歴から消す
ここは止めたあとの作業です。GitHub公式の手順を、ファイル名だけ例に合わせて写します。この節のコマンドは手元で動かしていないので、実行結果は載せません。先に、公式が挙げている副作用を確かめます。
| 副作用 | 内容 |
|---|---|
| 全部のハッシュが変わる | 該当のコミット以降、コミットの番号がすべて変わる |
| 古いクローンから戻る | 古いクローンの人がpull(更新の受け取り)のあとpushすると鍵が戻る |
| 他の人の作業が消える | 作業中のブランチ(変更を分けて作る枝)を上書きすると変更が失われる |
| プルリクエストの差分が消える | 閉じたプルリクエスト(Pull Request、PR。変更の合流の依頼)の差分が見られなくなる。開いているものは先にマージか閉じる |
| 署名が消える | コミットやタグの署名がなくなる |
| 鍵の場所を教えることになる | 古いクローンの人は、履歴の食い違いから鍵を見つけられる |
手順です。
git filter-repoを入れます。Macならbrew install git-filter-repoです。2.47以上が要ります。- 普段の作業フォルダではなく、新しくクローンします(下のコマンド①)。
.envのようにファイルごと消すなら、コマンド②を実行します。ファイル名だけでなくGit上のパス全体を書きます。send_mail.pyのようにファイルの中の文字列だけ消すなら、鍵を書いたテキストをリポジトリの外に作り、コマンド③で指定します。- 消えたかを確かめます。
git log -Sで探すのが手早い方法です。 - 影響するプルリクエストの数をコマンド④で数えます。多すぎるなら、ここで止められます。次の手順からは取り消せません。
- コマンド⑤で上書きのpush(force push)をします。
refs/pull/で始まるものは必ず失敗します。GitHubが読み取り専用にしているためです。それ以外が失敗したら、ブランチ保護(直接の変更を禁じる設定)を一時的に解除してやり直します。- プルリクエストが影響するときや、キャッシュも消したいときは、GitHub Support portalから連絡します。オーナー名とリポジトリ名、影響するプルリクエストの数、出力にある「NOTE: First Changed Commit(s)」の内容を伝えます。
- 他の人とフォークの持ち主に連絡します。
①
git clone https://github.com/sample-cooking/reservation-mail
cd reservation-mail
②
git-filter-repo --sensitive-data-removal --invert-paths --path .env
③
git-filter-repo --sensitive-data-removal --replace-text ../passwords.txt
④
grep -c '^refs/pull/.*/head$' .git/filter-repo/changed-refs
⑤
git push --force --mirror origin
連絡が要る理由は、force pushの先にサーバー側で何が残るかを見ると分かります。

書き換えても戻ってくる
git filter-repo が手元にないので、直前のコミットを --amend で作り直してforce pushする例で同じことを確かめます。.env が入ったコミットを送ったあと、外注エンジニアBがクローンを持っている状態です。
普通のpushは断られます。履歴が食い違うからです。--force-with-lease は、自分の知らない更新がリモートに入っていないときだけ上書きする、--force より安全な指定です。
$ git log --oneline
d0c788f 料金表を修正
c56d07c 最初のコミット
$ git push
To ../remote.git
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to '../remote.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
$ git push --force-with-lease
To ../remote.git
+ 714078a...d0c788f main -> main (forced update)
$ git log --all -S fake-key --oneline
手元では消えました。ところがサーバー側で古いコミットの番号を指定すると、まだ読めます。
$ git -C ../remote.git show 714078a:.env
MAIL_API_KEY=xxxxxxxx-fake-key
GitHubでも、古いコミットの掃除はサポートの作業です。これがサポートへ連絡する理由です。次に、Bの手元です(git pull の案内とパスの行は省略しました)。
$ git log -S fake-key --oneline
714078a 料金表を修正
$ git pull
fatal: Need to specify how to reconcile divergent branches.
$ git pull --no-rebase --no-edit
Merge made by the 'ort' strategy.
.gitignore | 1 +
1 file changed, 1 insertion(+)
create mode 100644 .gitignore
$ git ls-files
.env
.gitignore
menu.html
$ git push
d0c788f..c1f7042 main -> main
Bは悪気なくマージ(枝の合流)してpushしました。代表の側で確かめます。
$ git fetch
From ../remote
d0c788f..c1f7042 main -> origin/main
$ git log origin/main -S fake-key --oneline
714078a 料金表を修正
$ git show origin/main:.env
MAIL_API_KEY=xxxxxxxx-fake-key
消したはずの鍵が戻りました。公式が古いクローンの扱いを細かく書いているのは、このためです。
④他の人に知らせる
書き換えの前に作業を止めてもらい、終わったら次を伝えます。外注エンジニアBへの連絡はこの程度で足ります。
reservation-mailの履歴を書き換えました。
今のクローンは使わず、削除してクローンし直してください。
古いクローンからpushしないでください。
作業中の変更があれば先に連絡をお願いします。
公式は古い履歴のブランチについて「rebase, not merge」(合流させずに付け替える)と書いていますが、古いクローンを捨てる方が確実です。フォークは別です。持ち主に削除を頼むしかなく、GitHubは持ち主の連絡先を教えてくれません。
GitHubの検出機能の範囲
GitHubには、鍵らしいものを見つけるSecret scanningと、送る時点で止めるpush protectionがあります。使えるかどうかはリポジトリの種類で変わります(2026年11月時点)。
| 種類 | Secret scanning | push protection |
|---|---|---|
| 公開リポジトリ | 無料で自動 | 個人アカウントの設定で既定で有効 |
| Freeの非公開リポジトリ | 使えない | 使えない |
| Team・Enterprise Cloudの組織の非公開 | GitHub Secret Protectionを有効にすれば使える | 同じくSecret Protectionが必要 |
サンプル料理教室のように非公開で使っていると、この検出は働きません。手元で止める道具が要ります。
手元で止める道具 gitleaks
gitleaksは、リポジトリやファイルの中からパスワードやAPIキーを探す道具です。ライセンスはMITで、GitHubの公式手順でもコミット前の検査の例に挙がっています。コマンドは公式のREADMEから写しました。
brew install gitleaks
gitleaks git -v path_to_repo
gitleaks dir -v path_to_directory_or_file
git は履歴の検査、dir はフォルダやファイルの検査です。--redact を付けると、見つけた鍵を画面に出しません。コミットのたびに自動で走らせるなら、pre-commit という別の道具を使い、リポジトリの一番上に .pre-commit-config.yaml を置いて pre-commit install を実行します。
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.2
hooks:
- id: gitleaks
なお、READMEには機能開発を終えて今後はセキュリティの修正だけになるとあり、作者は開発の中心を Betterleaks という別の道具に移すと書いています(2026年11月時点)。検査に使う分には困りませんが、入れるときにREADMEの最新の案内を読みます。
再発を防ぐ
| 決めること | 中身 |
|---|---|
| 鍵はコードに書かない | send_mail.py は os.environ で読み、値は .env に置く |
.env はGitに入れない | .gitignore に書き、見本の .env.example だけ入れる |
| 鍵の原本は1か所 | パスワード管理ツールに集める。チャットやメールで渡さない |
| 送る前に検査 | ファイルを名指しして git add、git diff --cached で中身を見る、gitleaks |
| 鍵の範囲を絞る | 止めるときの影響が小さくなるよう、用途ごとに分けて期限を付ける |
| AIにも鍵を渡さない | 鍵をチャットに貼らない。読めないファイルに指定する |
最後の行は、AIと一緒にコードを直すようになって増えた話です。実際に運用していると、AIは動かすために .env を読もうとしますし、読んだ値をコードに直接書いてしまうこともあります。渡さない形にしておくのが確かです。Claude Codeなら、設定ファイル ~/.claude/settings.json の permissions に次を書くと、標準の読み取りと cat などのコマンドでは .env を読めなくなります。
"deny": [
"Read(./.env)",
"Read(./.env.*)"
]
完全には防げません。ファイル名を指定しない grep -r や、Pythonなどのスクリプトからの読み取りには効かないので、すべて止めるにはサンドボックス(OSの側で読み取りを止める仕組み)を使います。
個人情報を漏らしたとき
顧客の名簿や受講者の連絡先が入っていたら、鍵とは別の対応になります。鍵は作り直せますが、出た情報は取り戻せません。履歴から消す作業は同じようにやったうえで、誰の何が何件、いつからいつまで、誰に見える状態だったかを整理します。
法律上の報告と本人への通知が必要になる場合があります。法的な判断は自分で決めず、個人情報保護委員会のサイトの、漏えい等の対応のページを確認し、必要なら弁護士に相談します。公開か非公開かで見える範囲は変わるので、招待していた人の一覧も控えておきます。
できないことと、その代わり
| できないこと | その代わり |
|---|---|
| 他の人のクローンから消す | 消して取り直してもらう。鍵は止めてあるので実害は防げる |
| フォークから消す | 持ち主に頼む |
| 取られたかどうかを確かめる | 発行元の利用履歴を見る。分からなければ取られた前提で作り直す |
| Freeの非公開でGitHubに検出させる | gitleaksを手元で動かす。必要ならTeamとSecret Protectionを検討する |
| 出た個人情報を取り戻す | 影響範囲を整理し、報告や通知の要否を確認する |
よく詰まるところ
| 症状 | 原因 | 直し方 |
|---|---|---|
鍵を消したのに git log -p で見える | 消すコミットは履歴を消さない | 鍵を止めてから git filter-repo を使う |
--amend のあと git push が rejected になる | 履歴が食い違っている | 送ったあとなら他の人に伝えてから git push --force-with-lease |
| force pushが拒否される | ブランチ保護で禁止されている | 一時的に解除し、終わったら戻す |
refs/pull/ で失敗と出る | GitHubの読み取り専用の参照 | 正常。他に失敗がなければサポートへ連絡する |
| 消したはずの鍵が戻った | 古いクローンからマージしてpushされた | 全員にクローンを取り直してもらってから、もう一度消す |
git filter-repo が見つからない | Gitとは別の道具で、入っていない | brew install git-filter-repo で入れる |
| 鍵を止めたら予約メールが止まった | サーバーの .env が古い鍵のまま | 新しい鍵に差し替えて、送信を確かめる |
次に読む
置いてはいけないファイルの書き方は「.gitignoreの書き方と、リポジトリに入れてはいけないファイル」にあります。トークンの範囲や招待した人の見直しは「GitHubの権限と2段階認証:共同作業者・Organization・非公開リポジトリ」です。git revert や git reset の違いは「Gitで元に戻す:revert・reset・restoreの違いと使い分け」に、AIと一緒に使うときの注意は「GitHub CopilotとClaude Code:AIでコードを書くときのGitの使い方」にまとめています。