APIキーをコードから消してコミットし直したのに、GitHubで過去のコミットをたどると、前の版にそのまま残っている。これは事故というより、Gitの仕組みどおりの動きです。

この記事では、APIキー(外部サービスを使うための合言葉のような文字列)が、隠したつもりでも外から見えてしまう代表的な経路を3つと、出てしまったときにやることを整理します。「キーは環境変数に置く」というところまでは知っている人でも、移す前の履歴や、移したあとの扱いで漏れる経路が中心です。

例に出すキーはすべて架空の値です。Gitの動きは Git 2.54 で実際に試して確認しました。

経路1:コードに直書きして push した(あとで消しても履歴に残る)

まず押さえたいのがこれです。動作確認のつもりで、キーをそのままコードに書いてしまうパターンです。

// api.js(1回目のコミット)
const apiKey = "dummy-aaaa-1111";

あとで気づいて環境変数に移し、コミットし直したとします。

// api.js(2回目のコミット)
const apiKey = process.env.MY_API_KEY;

今のファイルからはキーが消えました。でも Git は「これまでの全部の版」を保存する仕組みなので、1回目のコミットには前の中身が残っています。キーの文字列で履歴を検索すると、ちゃんと見つかります。

git log -S "dummy-aaaa" --oneline
# 21aa28f 環境変数に移動      ← キーが消えたコミット
# be53a21 API呼び出しを追加   ← キーが入ったコミット

GitHubに push していれば、公開リポジトリなら誰でもこの過去の版を開けます。「キーを消したコミットを足す」だけでは、キーは隠れません。

経路2:.env.local をコミットしてから .gitignore に書いた

キーを .env.local に移すのは正しい対処です。ただし順番を間違えると、Gitへの登録は止まりません。

.gitignore は「まだGitに入っていないファイルを、これから無視する」ための設定です。すでに一度コミットしたファイルは、あとから .gitignore に書いても管理対象のまま残ります。実際に試すと、コミット済みのファイルは .gitignore に書いたあとも、書き換えるたびに「変更あり」と表示され続けました。

git status --short
#  M .env.local     ← 無視されず、変更として出てくる

管理対象から外すには、手元のファイルは残したまま、Gitの記録からだけ外すコマンドを使います。

git rm --cached .env.local
git commit -m ".env.local を管理対象から外す"

これで次からは無視されます。ただし経路1と同じで、過去のコミットに入った中身は残ったままです(push 済みなら、外からも見えます)。

.gitignore に書いたのに効いているか不安なときは、git check-ignore -v .env.local を実行してみてください。何も表示されない場合は、まだ管理対象に残っているか、.gitignore の書き方が合っていないサインです。

経路3:NEXTPUBLIC を付けて、ブラウザに配ってしまった

経路1・2のようにキーを環境変数へ移すと、画面側のコードでは値が undefined になることがあります。そこで「名前に NEXT_PUBLIC_ を付けたら動いた」で終わらせると、3つ目の経路が開きます。

Next.js では、名前に NEXT_PUBLIC_ を付けた環境変数を画面側のコードで使うと、その値がビルド(公開用にコードをまとめる作業)のときに、画面側のJavaScriptへ埋め込まれます。つまり、サイトを開いた人なら誰でも、ブラウザの開発者ツールから読める状態になります。

これは「見られても困らない値」を画面側で使うための仕組みです。たとえば Supabase では、ブラウザに出る前提の鍵と、絶対に出してはいけない鍵が分かれています。

  • NEXT_PUBLIC_SUPABASE_ANON_KEY … ブラウザに出る前提の鍵。データを守るのは RLS(行ごとに「誰が読めるか」を決める設定)の役目
  • SUPABASE_SERVICE_ROLE_KEY … RLS を素通りできる強い鍵。NEXT_PUBLIC_ を付けてはいけない

「画面から読めないとエラーになるから」と、強い鍵に NEXT_PUBLIC_ を付けて解決したことにすると、玄関の鍵をドアに貼っておくのと同じ状態になります。

画面から強い鍵が必要に見えたら、まず RLS のポリシーで「ログイン中の本人なら読める」と許可できないかを見直します(読めない原因が、鍵ではなくポリシーの側にあることもあります)。それでも強い鍵が要る処理だけをサーバー側(Route Handler や Server Action といった、サーバーで動く処理の置き場所)に移し、そこでも「呼び出した人がその操作をしてよいか」を確かめます。

なお、サーバー側で読んだ強い鍵を、画面側の部品に props(部品に渡すデータ)でそのまま渡した場合も、NEXT_PUBLIC_ を付けていなくてもブラウザに届きます。鍵はサーバー側の処理の中だけで使い、画面側には結果だけを渡します。

一度でもこの状態で公開していたら、NEXT_PUBLIC_ を外して直すだけでは足りません。配られたJavaScriptに鍵が入っていた以上、次の「鍵の作り直し」が必要です。

出てしまったら:消すより、鍵を作り直す

キーが外に出たと気づいたら、最初にやるのは履歴の掃除ではなく、発行元のサービスでそのキーを無効にして、新しいキーを発行し直すことです。

  • 公開リポジトリに一度でも push したキー、公開したサイトに埋め込まれたキーは、もう誰かにコピーされた前提で考える
  • 新しいキーは .env.local に入れ、本番(Vercel など)の環境変数も新しい値に更新して、再デプロイする(環境変数の変更は、次のデプロイから反映されます)
  • 履歴からの削除は、その後で必要に応じて行う(消しても、すでにコピーされた分は取り戻せないため)
  • 作り直しの手順や、作り直したときに一緒に変わるもの(ほかの鍵やログイン状態など)はサービスごとに違うので、発行元の案内に従う

GitHub にも、公開リポジトリへの秘密情報の push を止める仕組みがあります。ただ、すべての種類のキーを見分けられるわけではないので、それだけに頼るのは危険です。

AIが書いたコードを、コミットのときに見る場所

AIに「とりあえず動くようにして」と頼むと、手早く動かすために、キーを直接コードに書いた版が出てくることがあります。AIは速く書いてくれる分、「これを公開して大丈夫か」を見る目は人の側に残ります。コミットのときは、次の2つだけ見る習慣をつけると安心です。

git status            # .env.local が一覧に出ていないか
git diff --staged     # これから記録する行に、キーらしい長い文字列や、NEXT_PUBLIC_ が付いた強い鍵の名前がないか

AIに頼むときも「キーは環境変数から読む形で書いて。画面側(ブラウザで動くコード)には置かず、NEXTPUBLIC も付けないで」と一言添えておくと、3つの経路のどれかに入った版が出てくるのを防ぎやすくなります。

鍵は一度外に出ると、消しても取り戻せません。出る前にコミットの入口で止める、出たら作り直す。この2つを知っておくと、AIと一緒に速く作るときの怖さはぐっと減ります。


面談なしで、今日から始められます

この講座は 未経験から Next.js + Supabase + Claude Code で Webアプリを公開するまで を全24セッションで体系化した教材付きプランです。無料相談を挟まず、申し込んだその日から教材で学習を始められます。AIが学習パートナーになって何度でも質問でき、つまずいた所だけチャットで直接サポートします。

  • 今日から始める(教材完全版+月5,500円・チャット質問し放題・いつでも解約OK)→ https://menta.work/plan/20251?ref=menta-knowledge
  • いきなりは不安な方へ:無料の教材体験版もあります(最初の数セッション分)→ プラン詳細をご覧ください