AIが読んだ値はどこへ行くのか
.envはAPIキーやパスワードを一か所にまとめておくファイルです。APIキーは、プログラムが外部サービスに入るときに提示する通行証だと考えてください。このファイルをなぜコードと分けて置くのかは、前回の記事で扱いました。この記事ではその次の問いを扱います。そのファイルを自分のAIコーディングツールが開いたらどうなるのか。
Claude CodeとCodexは、自分のパソコン上で自分のアカウントの権限でコマンドを実行します。自分が開けるファイルはAIも開けるということです。そしてAIは次の行動を決めるためにコマンドの結果を読む必要があるので、結果は会話に入り、モデルを動かす会社のサーバーへ送られます。AnthropicのClaude Codeのデータ利用に関するドキュメントにも、ユーザーのプロンプトとモデルの出力はすべてネットワーク経由で送信されると書かれています。AIが.envを画面に出せば、パスワードはその会話の一部になります。
送られた後の保存期間は、アカウントの種類と設定によって異なります。同じドキュメントによると、個人プラン(Free・Pro・Max)では、モデル改善へのデータ提供を許可すると5年、許可しなければ30日で、チーム・エンタープライズ・APIアカウントは標準で30日です。設定はclaude.aiのプライバシー設定でいつでも変更できます。Codexもコマンドの結果をモデルに送らなければ動かない構造は同じで、保存の基準はOpenAI側のポリシーを別途確認する必要があります。
サーバーだけの問題でもありません。Claude Codeは途切れた会話を続けられるように、会話の記録を自分のパソコンの~/.claude/projects/フォルダに、暗号化されていないそのままの文字で標準30日間保存します。一度でも画面に出たパスワードは、そのファイルにも残ります。
読むことと見ることは違う
だからといって、AIに.envを使わせないわけにはいきません。プログラムはキーを読まなければ動きません。ここで区別すべきことが二つあります。プログラムが値を読むことと、AIが値を見ることです。
AIが`source .env && python3 run.py`を実行すると、.envを開くのはrun.pyというプログラムです。パスワードは自分のパソコンから外部サービスへ行くだけで、AIに返ってくるのはプログラムが画面に出した"完了"のような出力だけです。`curl -H "Authorization: Bearer $SLACK_TOKEN"`のように、コマンドに変数名だけを書く場合も同じです。名前を実際の値に置き換えるのは、自分のパソコンのシェルです。シェルとは、ターミナル、つまりコマンドを文字で入力するウィンドウからそのコマンドを受け取って実行するプログラムです。
漏れるのは、AIが値を画面に出すコマンドを実行するときです。`cat .env`でファイルの内容をそのまま出したり、`echo $SLACK_TOKEN`で変数の値を出したり(変数とは名札を付けて値を入れておく枠です)、保存された値をすべて一覧表示する`printenv`を実行したりする場合です。止めるべき対象はファイルへのアクセス全体ではなく、この出力です。
.env ──> python3 run.py ──> 外部サービス (値の行き先)
│
└──> 画面出力 "完了" ──> AI ──> モデルのサーバー (AIが見るもの)伏せてくれるのは約束であって仕組みではない
この問いを初めて投げた日、Claudeはすでに値を伏せていました。ファイルを読むときに値の部分を<set>に置き換えるコマンドを付けて、キーの名前だけを見せていました。良い習慣ですが、これはAIが自分で守るルールです。
ルールは二つの場合に破られます。一つはAIのミスです。不具合の原因を探すデバッグの最中に、値を確認しようとしてそのまま出してしまうことがあります。もう一つはプロンプトインジェクションです。AIが読んだWebページや文書の中に隠された指示文がAIをだまし、本来しない行動をさせる攻撃のことです。どちらの場合も、AIの善意では防げません。
そこでこの記事の答えはこうです。ClaudeとCodexの意図を信じるかどうかで悩むより、意図に関係なく値が外に出られない構造を先に作ります。人間の従業員に金庫の暗証番号を教えず、金庫を開ける権限だけを与えるのと同じです。
ステップ1:Claude Codeの.env読み取り禁止ルール
最も簡単な一重目は、Claude Codeが公式に提供している権限ルールです。ホームフォルダの.claude/settings.jsonファイルに以下の内容を入れます。settings.jsonはClaude Codeの動作を決める設定ファイルです。ファイルがすでにある場合は、permissionsの中のdenyリストに2行だけ追加します。
公式ドキュメントによると、このルールはClaudeのファイル読み取りツールだけでなく、シェルで使うcat・head・tail・sedのようなファイルコマンドや、`< file`のような入力のリダイレクトにも適用されます。`//**/`は、パソコンのどこにある.envでも対象になるという意味です。`.env.*`の行は.env.localのようなファイルを止めますが、共有用の見本ファイルである.env.exampleも一緒に止まります。
限界もドキュメントに書かれています。ファイル名を指定せずにフォルダ全体から文字を探す`grep -r`や、Python・Node(どちらもプログラミング言語です)で書いたスクリプトが自分でファイルを開く場合には適用されません。OSのレベルですべてのプログラムのアクセスを止めたいなら、サンドボックスを有効にするよう案内しています。サンドボックスとは、AIが実行するコマンドを決められた囲いの中だけで動かす隔離の仕組みです。また、このルールはClaude Codeの設定なので、Codexには適用されません。
{
"permissions": {
"deny": [
"Read(//**/.env)",
"Read(//**/.env.*)"
]
}
}ステップ2:フック — コマンドが実行される直前の検問所
フックとは、AIツールが特定のタイミングで自分のスクリプトを自動的に呼び出すようにするつなぎ目です。そのうちPreToolUseは、AIがコマンドやツールを実行する直前に呼ばれます。スクリプトはAIが何を実行しようとしているかを受け取って確認し、止めるべきなら拒否の信号を返します。するとコマンドは実行されず、拒否の理由がAIに伝えられます。Claude CodeとCodexはこの拒否の信号の形式が同じなので、一つのスクリプトを両方にかけられます。
以下は、ステップ1のルールが見逃すケースのうち、最もよくある二つだけを止める短い例です。保存された値を丸ごと一覧表示するenv・printenvの単独実行と、名前にTOKEN・SECRET・PASSWORD・_PW・_KEYが入った変数をechoやprintfで出すコマンドです。~/.claude/hooks/block-env-print.pyとして保存します。Macにpython3がインストールされている必要があります。
import json, re, sys
cmd = json.load(sys.stdin).get("tool_input", {}).get("command", "")
secret = r"\$\{?[A-Z0-9_]*(TOKEN|SECRET|PASSWORD|_PW|_KEY)[A-Z0-9_]*"
if re.fullmatch(r"\s*(env|printenv|export -p)\s*", cmd) or re.search(r"(echo|printf)[^|;&]*" + secret, cmd):
print(json.dumps({"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "秘密の値を画面に出すコマンドのため止めました。値は使うだけにして出力しないでください。",
}}, ensure_ascii=False))
フックをClaude CodeとCodexにかける方法
Claude Codeでは、同じsettings.jsonにhooks項目を追加します。matcherはどのツールに検問所を置くかを選ぶ欄で、Bashはシェルコマンドを実行するツールです。ステップ1のpermissionsと一つのファイルに並べて置きます。
Codexでは、~/.codex/hooks.jsonに同じ形で書きます。違う点が一つあります。Codexは、ユーザーがフックの定義を自分で確認して信頼しないと実行しません。Codexを開いて/hooksコマンドで新しく追加された項目を信頼済みにするまでは、フックはスキップされます。
かけた後は一度テストします。この例にサンプルのコマンドを10個入れてみたところ、値を出すコマンド5個は止まり、値を使うだけのコマンド、文字数だけを見るコマンド、秘密と無関係なコマンドの計5個は通りました。そのうち六つを下の表に載せました。実際のツールでは、AIにprintenvを実行するよう頼んで、止まるかどうかを確認すればよいです。
| コマンド | 結果 | 理由 |
|---|---|---|
| printenv | ブロック | 保存された値をすべて一覧表示する |
| echo $SLACK_TOKEN | ブロック | トークンの値を画面に出す |
| printf "%s" "$OPENAI_API_KEY" | ブロック | APIキーの値を画面に出す |
| source .env && python3 run.py | 許可 | プログラムが値を使うだけ |
| curl -H "Authorization: Bearer $SLACK_TOKEN" … | 許可 | コマンドには変数名しかない |
| echo ${#SLACK_TOKEN} | 許可 | 値ではなく文字数だけを出す |
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 \"$HOME/.claude/hooks/block-env-print.py\""
}
]
}
]
}
}{
"hooks": {
"PreToolUse": [
{
"matcher": "^Bash$",
"hooks": [
{
"type": "command",
"command": "python3 \"$HOME/.claude/hooks/block-env-print.py\""
}
]
}
]
}
}実際にかけたフック:止めるもの、通すもの、止められないもの

例は入り口です。実際に使っているフックは、同じ原理でケースを広げた一つのPythonスクリプトで、2026年9月11日にClaude CodeとCodexの両方にかけました。ルールを直すたびに、止めるべきコマンド39件、通すべきコマンド51件、ツールごとのケース11件、解析失敗1件の計102件を一度に回し、両方向とも正しいかを確認しています。止めるときに勧める代替コマンドも許可リストに入れておき、案内どおりに直したのにまた止まることがないようにしました。
解析失敗の1件は、わざと通すケースです。スクリプトがコマンドを解析できない場合は、止めずに実行を許可します。あいまいなコマンドをすべて止めると作業が止まってしまうために選んだ折衷案であり、その分だけ穴でもあります。
かけてからの2日間で、このフックは私のAIを3回止めました。9月12日には、APIキーの変数をprintfで出そうとしたコマンドと、Pythonのコードの中で.envを開いていたコマンドが止まりました。AIは拒否の理由を読み、値を出さない形に変えて再実行しました。9月13日には、この記事を書くために過去の会話の記録を検索していたコードが止まりました。検索語のリストに".env"という文字が入っていただけで、実際に.envを開いてはいなかったので、止めなくてよいものを止めた誤検知です。AIはコードをスクリプトファイルに移して実行しました。この最後の場面が、フックの限界をそのまま示しています。スクリプトファイルの中で何をしているかは、コマンド1行を見ただけではわかりません。下の表のcat・grep・sed・jq・cutは、いずれもファイルの内容を画面に表示したり、一部だけを抜き出したりするコマンドです。
| 区分 | 例 |
|---|---|
| ブロック | cat・head・grep・sed・jqで.envの内容を出力、ファイル読み取りツールで.envを開く、env・printenvの単独実行、秘密の名前の変数をecho、Python・Nodeの1行コードが.envを開く場合、除外条件なしにフォルダ全体から秘密の名前をgrep |
| 許可 | source .envの後にプログラムを実行、値の部分を伏せるsed、行数だけを数えるgrep -c、キーの名前だけを抜き出すcut・jq keys、ファイルサイズ・更新日の確認、結果をファイルに保存 |
| 止められない | スクリプトファイルが自分で値を出力、set -xやcurl -vのようにコマンドが処理過程を詳しく出すオプション、ファイルを別の名前でコピーしてから読む、解析失敗時の許可、Codexのウェブ検索のようにフックを通らないツール |
100%でなくても先にかけるべき理由と、次の手順
フックはコマンドを文字としてしか見ません。そのため止める範囲を広げるほど誤検知が増え、回避しようとするAIを最後まで止めることはできません。それでも事故の多くは悪意より習慣から起きます。デバッグ中に何気なく出すcat .env、設定を確認しようとして実行するprintenvがその習慣であり、この二つを止めるにはルール2行と短いスクリプトがあれば十分です。
次の手順は三つです。第一に、すでに会話に値を出したことがあるなら、そのキーを再発行します。会話の記録から消すより、キーを変えるほうが確実です。第二に、プロジェクトごとに、そのプロジェクトに必要なキーだけを入れた.envを別に置きます。キーが数十個入ったファイル一つをすべてのプロジェクトが読み込むと、一度のミスでその数十個が一度に出ていきます。第三に、使っているプランのデータ設定を一度確認します。
今日やることは一つです。~/.claude/settings.jsonに.env読み取り禁止ルールの2行を入れ、Claude Codeに"cat .envを実行して"と頼んで、止まるかどうかを確認します。
