AIにAPIキーを渡していませんか
始まりは1つのechoコマンド
きっかけは、AIに頼んだ環境変数の確認コマンドだった。
「OPENAI_API_KEYが設定されているか確認して」と頼んで、Claude Codeが書いてきたのがこれ。
echo "API key set: ${OPENAI_API_KEY:+yes}${OPENAI_API_KEY:-no}"
実行した瞬間、ターミナルに164文字のキー全文が表示された。
「設定済みならyes、未設定ならno」のつもりだったコマンドだが、${VAR:-default} は変数があれば default ではなく値そのものを返す仕様。これに引っかかった。
長い文字列が出た時点で「あ、これ全部出てる」と気づいて止めた。けれど画面に出した瞬間にClaudeはもう会話ログに記録している。Anthropic側のログにも、ローカルのClaude Codeセッションファイル(~/.claude/projects/)にも、もう値は残っている。
そこで終わりにしてもよかった
1個のキーをrotateして閉じる、で済んだ話。
ただ、自分の運用を振り返って、もっと根本的な問題があることに気づいた。
私はAPIキーをずっとチャットに貼ってきた
OpenAIで新しいキーを取得して、そのままClaude Codeとの会話に貼り付けて、画像生成のスクリプトを動かしてもらう。Geminiでも同じ。新しいAPIサービスを試すたびに、キーをそのままチャットに貼って、AIにコードを書いてもらって、動かしてきた。
これは多くの開発者が無意識にやっているパターンで、私も例外じゃなかった。
問題は、チャットに貼った瞬間にそのキーが永久保存されることだ。
- ローカルのClaude Codeセッションファイル
- Anthropic側のログ(規約上、品質改善・安全性チェックのために保管される)
- 過去の会話を引き継いでセッションを再開すると、コンテキストとして再ロードされる
つまり、1回貼ったキーは、コードリポジトリにcommitしてしまったキーと同じ取り扱いをすべき。漏洩したものとして扱う必要がある。
連鎖的に発覚した棚卸し
そこから過去のClaude Code会話ログを全部スキャンした。
| 種類 | ヒット数 | 判定 |
|---|---|---|
| OpenAI sk-proj- | 1 | 露出済み(さっきの事故) |
| Anthropic sk-ant- | 2 | 個人キー無し、Claude Code内部由来 |
| Google AIzaSy | 4 | うち1個が現役のGemini APIキー |
| GitHub ghp_ | 0 | クリーン |
| AWS AKIA | 0 | クリーン |
| Stripe sk_live_ | 1 | 29文字テンプレ、偽陽性 |
| Postgres URL | 7 | うち1個に実際のSupabase接続文字列 |
| JWT (eyJ...) | 6 | うち3個がSupabase JWT形式 |
Geminiの現役キーが過去ログに残っていた事実は地味にきつかった。「漏れてない」と思っていたものが、自分の不注意で漏れていた。
結局やったこと
- OpenAIキーをrotate
- Geminiキーをrotate
- Supabaseの休眠プロジェクト3個を全削除(漏洩した接続文字列を物理的に無効化)
- 公開GitHubリポ25個を別途スキャン → こちらは全部プレースホルダのみで実害なし
時間にして30分。一日の予定を1コマつぶして対応した。
安全な型を1つだけ覚える
これからは原則を1つに絞った。
APIキーをClaudeに見せない。AIに「この環境変数を使うコードを書いて」と指示する。
具体的にはこうなる。
やめた書き方:
「OpenAIで画像生成して。キーは sk-proj-xxxxx です」
新しい書き方:
「OpenAIで画像生成して。OPENAI_API_KEY は ~/.zshrc に設定済み」
差分は2行だが、意味が違う。AIが書くPythonコードは os.environ['OPENAI_API_KEY'] を読むだけなので、キーの値そのものはAIの目に触れない。
環境変数の存在確認も、値を出さないコマンドに統一した。
# 存在確認
[ -n "$OPENAI_API_KEY" ] && echo "OK" || echo "NG"
# 識別用の部分表示
echo "先頭8文字: ${OPENAI_API_KEY:0:8}"
echo "長さ: ${#OPENAI_API_KEY}"
echo "$VAR" も printenv VAR も、もう書かない。
医療現場への接続
このパターンは、AIに患者IDや診療情報を扱わせるときも同じ構造になる。
AIにテキストを直接貼って「このカルテをまとめて」と頼む運用は、APIキーをチャットに貼る運用と等価で危険。AIには「カルテファイルから読み込んで処理する関数を書いて」と頼むのが正しい設計だ。機密情報はAIの目に直接入れず、コード経由で扱う。
医療AIで起きうる事故の輪郭は、開発者がAPIキーで日常的にやっている運用ミスと相似形になっている。先に開発の現場で型を整えておくことが、医療データを扱うときのリテラシーになる。
自分の過去ログを棚卸しするコマンド
過去のClaude Code履歴をスキャンしたい人向けに、コマンドだけ置いておく。値を出さない設計にしてある。
CLAUDE_DIR="$HOME/.claude/projects"
echo "OpenAI: $(grep -rho 'sk-proj-[A-Za-z0-9_-]*' $CLAUDE_DIR 2>/dev/null | awk 'length($0)>=40' | sort -u | wc -l) ユニーク値"
echo "Anthropic: $(grep -rho 'sk-ant-[A-Za-z0-9_-]*' $CLAUDE_DIR 2>/dev/null | awk 'length($0)>=40' | sort -u | wc -l) ユニーク値"
echo "Google: $(grep -rhoE 'AIza[A-Za-z0-9_-]{30,}' $CLAUDE_DIR 2>/dev/null | sort -u | wc -l) ユニーク値"
echo "GitHub: $(grep -rhoE 'gh[ps]_[A-Za-z0-9]{30,}' $CLAUDE_DIR 2>/dev/null | sort -u | wc -l) ユニーク値"
echo "AWS: $(grep -rhoE 'AKIA[A-Z0-9]{16}' $CLAUDE_DIR 2>/dev/null | sort -u | wc -l) ユニーク値"
ヒットがあれば、それは「漏れていた」キーだ。
該当キーを発行したサービスのダッシュボードでrotateするか、もう使っていないサービスならプロジェクトごと削除する。プロジェクト削除は、家ごと取り壊すようなもので、漏れた鍵が物理的に無意味になる。一番クリーンな解決法だ。
一行でまとめる
AIに渡したものは、コードに書いたのと同じ重さで扱う。
これだけ覚えていれば、たぶんもう同じ事故は起きない。