創刊準備号 · 2026年秋English · RSS
AgentKoji

Claude Codeに秘密を渡さない

Claude CodeのAPIキー管理は、denyだけでは足りない。grepやPythonは.envを読める。ダミーの鍵で漏れ道を再現した。

Claude Code に .env を読ませないための定番は、権限の deny に Read(./.env) を書くことです。ただ、それだけでは塞がらない経路があります。この記事では、公式ドキュメントが自ら「対象外」としている経路を、ダミーの鍵で再現します。そのうえで、鍵をエージェントの手の届く範囲に置かない設計をまとめます。

権限ルールの評価順そのものは「AIエージェントの権限設計」に書きました。

再現の条件

Claude Code のセッションの中で、deny がどの呼び出しを止めるかまでは観察していません。止まる・止まらないの判断は公式ドキュメントの記述に基づいています。ここで確かめたのは、「止まらない側の経路では、実際に中身が出る」ことです。

公式が書いている deny の範囲

公式ドキュメントの要点です(出典:https://code.claude.com/docs/en/permissions 、2026-10-16確認)。

止まらない経路を、ダミーの鍵で再現した

d=$(mktemp -d) && cd "$d"
env -i PATH=/usr/bin:/bin HOME="$d" bash --norc --noprofile
printf 'DUMMY_API_KEY=sk-dummy-0000-NOT-REAL\n' > .env
経路コマンド出力
ファイル名を書かない検索grep -r DUMMY_API_KEY ../.env:DUMMY_API_KEY=sk-dummy-0000-NOT-REAL
Python が自分で開くpython3 -c "print(open('.env').read())"DUMMY_API_KEY=sk-dummy-0000-NOT-REAL
Node が自分で開くnode -e "...readFileSync('.env')..."DUMMY_API_KEY=sk-dummy-0000-NOT-REAL
環境変数の継承export したあと bash -c 'env | grep DUMMY'DUMMY_API_KEY=sk-dummy-0000-NOT-REAL
シェルのトレースbash -x -c 'curl -H "Authorization: Bearer $DUMMY_API_KEY" ...'+ curl ... 'Authorization: Bearer sk-dummy-0000-NOT-REAL' ...

最後の行のように、デバッグのために set -x を付けると、展開された鍵がトレースにそのまま出ます。トレースはログにも、エージェントの文脈にも残ります。

ファイルを読ませない設定と、鍵を見せない設計は、別のものです。 deny は前者だけを担います。

環境変数も子プロセスに渡る

Claude Code を起動したシェルに鍵を export していると、Bash のコマンド、hook、stdio の MCP サーバーに、その環境が引き継がれます。公式は、CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 を設定すると、これらのサブプロセスの環境から認証情報を取り除く、と説明しています。判定は変数名か値の形で行うので、GITHUB_TOKEN や、名前も値も認証情報らしく見えない秘密は残ります。そのため、狭い権限ルールと組み合わせて使うよう書いています(出典:https://code.claude.com/docs/en/env-vars 、2026-10-16確認)。

hook のプロセスも親の環境を引き継ぎます(出典:https://code.claude.com/docs/en/hooks 、2026-10-16確認)。

環境変数が設定どおりに効かないときの切り分けは、「Claude Codeで環境変数が効かない」にまとめました。

OS のレベルで閉じる

公式のサンドボックスは、Bash のコマンドとその子プロセスについて、ファイルの読み取りとネットワークを OS のレベルで制限します。~/.ssh のような認証情報のファイルは filesystem.denyRead で閉じられます。macOS は Seatbelt、Linux と WSL2 は bubblewrap を使います(出典:https://code.claude.com/docs/en/sandboxing 、2026-10-16確認)。

逆に、denyRead は組み込みの Read ツールを止めません(同出典)。権限の deny とサンドボックスは、両方を設定して初めて隙間がなくなります。

PreToolUse の hook で、コマンドの文字列に .env が含まれていたら止めることもできます。書き方は「Claude Code hooks実践」に書きました。ただし、上の grep -r . のように、ファイル名が文字列に出てこない読み方は、hook では拾えません。

鍵を取り出すときの末尾の改行

OS の鍵保管庫から鍵を取り出すコマンドには、出力の末尾に改行を付けるものがあります。編集部では、それをそのまま secret として登録していました。鍵は改行込みで保存され、通知の経路が毎回失敗して、誰にも知らせずに止まっていました。

ダミーの値で、違いを確かめました。

get(){ printf 'sk-dummy-0000-NOT-REAL\n'; }   # 末尾に改行を付けて返すコマンドの代わり
get | wc -c                       # → 23 bytes(改行込み)
s="$(get)"; printf %s "$s" | wc -c   # → 22 bytes

パイプでそのまま渡すと、改行も一緒に渡ります。コマンド置換 $(...) で受けると、末尾の改行が落ちます。登録する前に、バイト数を確かめます。

エージェントには集計だけを見せる

個人情報を含むデータを調べさせるときは、個別の行を出力させません。個人情報ではない列だけを選び、件数や合計のような集計値だけを出させます。会話の記録には、出力がそのまま残るからです。

外部に送る操作(Web の取得、Issue の本文、別のモデルへのレビュー依頼)の前にも、渡すファイルに秘密が含まれていないかを確かめます。別のモデルへのレビュー依頼は、確認の対象から漏れやすい経路です。

鍵そのものを置かない

いちばん確実なのは、エージェントが動く環境に、平文の鍵を置かないことです。たとえば 1Password CLI の op run は、.env に書いた参照(op://...)を実行時に解決し、指定したコマンドのサブプロセスにだけ環境変数として渡します(出典:https://developer.1password.com/docs/cli/secrets-environment-variables/ 、2026-10-16確認)。

ただし、同じページは「同じユーザーが動かすプロセスは、ほかのプロセスの環境を読めると考えるべきだ」とも書いています(同出典)。渡した先のプロセスの中では、鍵は普通の環境変数です。

通知用のトークンを、LLM の文脈ではなく専用のスクリプトに分けた例は、「AIエージェントをVPSで24時間動かす」に書きました。

まとめ