はじめに
本記事では、~/.zshrc に直書きしたトークンや API キーを macOS のキーチェーンに移し、シェルの起動時に取り出す方法を紹介します。
要点は、次の1行の置き換えです。.zshrc には値を書かず、キーチェーンから値を取り出すコマンドを書きます。
- export OPENAI_API_KEY='sk-proj-abc123...'
+ export OPENAI_API_KEY="$(security find-generic-password -a "$USER" -s zshrc-OPENAI_API_KEY -w)"
背景:.zshrc の直書きは、AI エージェントの作業で漏えいする
API キーを .zshrc に export で書いておく方法は、昔から広く使われています。
export OPENAI_API_KEY='sk-proj-abc123...'
export GEMINI_API_KEY='AIzaSy...'
Claude Code などの AI エージェントは、頼まれた作業とは別に、環境を調べるために .zshrc を読むことがあります。利用者が値を見せるつもりでなくても、次のような場面で、値がツールの実行結果に含まれます。
- 利用者が PATH やシェルの設定を直すよう頼むと、AI エージェントは
.zshrc全体を読みます。 - AI エージェントは、特定の行を直すときに、確認のため前後の行も読みます。トークンの行が近くにあれば、その行も一緒に読まれます。
- AI エージェントは、設定の場所を探すために、ホームディレクトリのドットファイルを
grepします。一致した行は、値ごと出力されます。 - 利用者が Mac の移行やドットファイルの整理を頼むと、AI エージェントは
.zshrcの中身を一通り読みます。
たとえば、AI エージェントが PATH の設定を確かめるために次のコマンドを実行すると、すぐ下の行にあるトークンも出力されます。
$ sed -n '55,60p' ~/.zshrc
# Created by `pipx`
export PATH="$PATH:$HOME/.local/bin"
# ChatGPT - openai
export OPENAI_API_KEY='sk-proj-abc123...'
問題は、値が画面に表示されることではありません。ツールの実行結果がコンテキスト(モデルへの入力)に入ることです。実行結果がコンテキストに入った時点で、次の2つが起きます。
- 値がサービスの提供元に送信されます。 AI エージェントは、ツールの実行結果を次のリクエストに含めてモデルに送ります。Claude Code を Anthropic の API で使う場合、送信先は Anthropic です。端末で省略表示されていても、実行結果はモデルに送られます。
-
値がローカルの会話の記録に残ります。 Claude Code は、会話の記録を
~/.claude/projects/<作業ディレクトリ>/<セッションID>.jsonlに保存します。ツールの実行結果も、そのままの形で保存されます。記録は既定で30日間残ります(cleanupPeriodDaysで変えられます)。
{
"type": "user",
"message": {
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01...",
"content": "# ChatGPT - openai\nexport OPENAI_API_KEY='sk-proj-abc123...'\n"
}
]
}
}
人間が作業するときは、値の書かれた行を読み飛ばせました。AI エージェントは、ファイルを読んだ時点で値をコンテキストに入れてしまいます。一度送信した値は、あとから取り消せません。
.zshrc や .env に平文で書いた値は、以前から、リポジトリへの誤った commit などで漏えいすることがありました。AI エージェントを使うようになって、漏えいの経路がさらに増えています。
対策:キーチェーンに保存し、シェルの起動時に取り出す
macOS には、パスワードや鍵を暗号化して保存するキーチェーンがあります。security コマンドを使うと、ターミナルからキーチェーンに値を保存したり、取り出したりできます。
保存する
security add-generic-password で、値をキーチェーンに保存します。-w を最後に置き、値を書かずに実行すると、security が対話型(インタラクティブ)で値の入力を求めるので、そこに値を貼り付けます。
$ security add-generic-password -U -a "$USER" -s zshrc-OPENAI_API_KEY -w
password data for new item: ← 値を貼り付ける(画面には表示されません)
retype password for new item: ← もう一度貼り付ける
各オプションの意味は次のとおりです。
| オプション | 意味 |
|---|---|
-a "$USER" |
アカウント名。自分のユーザー名にします。 |
-s zshrc-OPENAI_API_KEY |
サービス名。取り出すときに、この名前で項目を探します。 |
-w |
値。最後に置いて値を書かないと、対話型で値を入力できます。 |
-U |
同じ項目が既にあれば、上書きします。 |
-w sk-proj-abc123... のように値をコマンドに直接書くと、実行中は ps の出力に、実行後は ~/.zsh_history に、値が平文で残ります。
サービス名は zshrc-<変数名> にそろえておくと便利です。「キーチェーンアクセス」で「zshrc-」と検索すると、保存した項目を一覧できます。
キーチェーンから取り出す
.zshrc の export の行を、キーチェーンから値を取り出すコマンドに置き換えます。
- export OPENAI_API_KEY='sk-proj-abc123...'
+ export OPENAI_API_KEY="$(security find-generic-password -a "$USER" -s zshrc-OPENAI_API_KEY -w)"
security find-generic-password に -w を付けると、値だけを出力します。シェルは起動するたびにこのコマンドを実行し、出力された値を環境変数に入れます。
確かめる
新しいタブを開き、値の先頭の数文字と文字数だけを表示して、値が入っていることを確かめます。先頭の sk- は、キーの種類を表す固定の文字列です。表示しても、値の推測にはつながりません。
$ echo "${OPENAI_API_KEY[1,4]}... (${#OPENAI_API_KEY} 文字)"
sk-p... (51 文字)
この方法で防げること、防げないこと
キーチェーンが守るのは、ファイルに書かれた値です。シェルが起動して環境変数に入った値までは守れません。
| 漏えいの経路 | 防げるか |
|---|---|
AI エージェントが .zshrc を読む。利用者が .zshrc を表示する、共有する |
防げます |
利用者が .zshrc を dotfiles リポジトリに commit する |
防げます |
シェルの起動後に、環境変数を表示する(例:AI エージェントが調査のために env | grep -i api を実行する) |
防げません |
同じユーザーで動くプログラムが、security で値を取り出す |
防げません |
最後の行について補足します。security コマンドで保存した項目は、security コマンドからなら確認のダイアログなしで取り出せます。キーチェーンのロックが解除されている間は、同じユーザーで動くスクリプトも、同じ方法で値を取り出せます。
また、キーチェーンがロックされている環境では、security が値を取り出せず、環境変数が空になります。SSH でログインしたシェルや、cron から実行するジョブが該当します。
キーチェーンに移しても、すでに漏えいした値は無効になりません。AI エージェントに .zshrc を読ませたことがあるなら、その値は作り直してください。
おわりに
AI エージェントが利用者の環境のファイルを読むようになり、.zshrc に直書きした値は、以前より漏えいしやすくなりました。今回紹介した方法をはじめ、平文の認証情報を撲滅する手法は様々あります。少しでも安全に開発できるよう対策していきましょう。