こんにちは、サイオステクノロジー武井です。
AI コーディングエージェントを使い始めて、ふと不安になったことはないでしょうか。
「このAI、プロジェクトの中を自由に読めるけど…….env みたいな機密情報も読めてしまうのでは?」
そうなんです。読めちゃうんです。しかも、ただ読めるだけでは終わりません。読んだ内容がドキュメントに紛れ込んだり、コミットに含まれたりして、GitHub リポジトリ経由で外部に漏れるところまで想像すると、なかなか怖い話です((((;゚Д゚))))ガクガクブルブル
この記事では、その対策として用意されている Claude Code の権限機能とサンドボックス機能を取り上げます。ただ「こう設定すれば安全」という話で終わらせるのではなく、なぜその機能が必要なのか、どこまで守れて、どこに穴が残るのかを、OS の仕組みまで降りて整理していきます。途中で Linux カーネルの fork/exec という基本機能まで出てきますが、そこが腑に落ちると、サンドボックスが「なぜそういう形をしているのか」まで見通せるようになります。
なお、以降は Claude Code を題材に話を進めますが、基本的な仕組みは他のコーディングエージェントでも多分同じ です。設定ファイルの名前や項目名は製品ごとに違いますが、どれも「エージェント本体のプロセスがファイルを読む」「シェルコマンドを子プロセスとして起動する」という同じ形で動いていて、OS が用意している道具立て(macOS の Seatbelt、Linux の namespace など)も共通です。この記事で扱うのは、その 共通部分の構造 です。「アプリが自分で我慢しているのか、OS が強制しているのか」という見分け方を持っておけば、お使いのツールがどちらの層で守ってくれているのかも、同じ物差しで判断できるようになります。
何も気をつけないと、AIは機密情報を読んでしまう
まず、問題の全体像を確認します。
多くのプロジェクトでは、API キーやデータベースの接続文字列といった秘密情報を .env ファイルに書いて、プロジェクト直下に置いています。ローカルで開発する分には便利な運用です。
ところが、AI コーディングエージェントはプロジェクト内のファイルを読んで文脈を理解します。つまり、放っておくと .env も読めるということです。そして厄介なのは、読んだ後に何が起きるか予測しづらい点です。
- 「README を書いて」と頼んだら、環境変数の設定例として本物のキーが埋め込まれる
- コード生成の過程で、秘密情報を含むファイルを参照した内容がコメントに残る
- それらをコミットして
git pushした瞬間、公開リポジトリに秘密が載る
「AIが勝手に漏らした」というより、「AIが読めたものが、生成物を経由して、いつの間にか外に出ていく」という流れです。悪意がなくても起きるからこそ、仕組みで防いでおく必要があります。
まず思いつく対策 ―― でも、これには穴がある
Claude Code には、こうした事故を防ぐための権限機能があります。settings.json の permissions.deny に、読ませたくないファイルを書いておく方法です。
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)"
]
}
}
これで Read ツールから .env へのアクセスがブロックされます。Glob や Edit など、ファイルに触れる他のツールについても同様に指定できます。
一見これで安心に見えますが、大きな穴があります。
permissions.deny がブロックしているのは、あくまで Claude Code というプログラム自身が持つ機能(Read ツールなど) です。言い換えると、これは アプリケーションが自分で「このファイルは読まない」と自制しているだけ なのです。
では、その自制を回避する経路があったらどうなるでしょうか。あります。そう、それは、、、、なんと!!Bash ツールです。
cat .env も diff .env other も、Read ツールを使っていません。別プロセスとして起動されるシェルコマンドです。だから「Read ツールを塞ぐ」というアプリレベルの自制は、ここには効きません。正面玄関に鍵をかけても、裏口が開いている状態です。
この「裏口」を塞ぐために登場するのが、サンドボックスです。
サンドボックスとは何か ―― システムコールそのものを止める
アプリの自制ではなく、OSの強制
permissions.deny が「アプリの自制」だったのに対し、サンドボックスは OS レベルの強制 です。
Claude Code が Bash コマンドを実行するとき、そのコマンドを サンドボックスで包んで起動 します。すると、そのコマンドは「特定のファイルにアクセスするシステムコール自体を呼べない」状態になります。cat だろうが diff だろうが Python スクリプトだろうが、ファイルを読むには必ず OS(カーネル)に open() というシステムコールを発行しなければなりません。サンドボックスはこの カーネルへの入り口 で検問を張るので、どんなプログラムを使っても迂回できません。
どのレイヤーで検問しているのか
「アプリの自制」と「OS の強制」は、ソフトウェアの層で見るとまったく違う場所に立っています。図にすると、こうです。
上から順に見ていきます。
アプリケーション層 : permissions.deny はここにいます。この層の特徴は、判定ロジックも判定対象も同じプロセスの中にある ことです。Claude Code が「.env を読もうとしている自分」を、自分のコードで止めている。つまり自主規制です。自主規制の弱点は明確で、そのルールを持っていないコードに仕事を渡せば、ルールは適用されない こと。cat .env は Claude Code のコードではないので、deny の判定ロジックをそもそも通りません。
システムコールの境界 : プロセスは自分の中では何でもできますが、ファイルを読む、ネットワークに出る、といった「外の世界に触る操作」だけは自分では実行できません。必ず open() connect() といったシステムコールでカーネルに依頼する必要がある。ここがプロセスの自由が終わる線です。
そして重要なのは、この線が プログラムの種類に依存しない こと。cat でも diff でも Python でも、自作のバイナリでも、ファイルを読むなら全員この一本の線を越えます。だから、この線の上に検問を置けば、「別のプログラムを使う」という迂回が原理的に成立しません。アプリ層の検問が「使うツールを変える」で抜けられたのと、ちょうど対照的です。
OS カーネル層 : 実際の判定はこの層で行われます。sandbox-exec が登録したポリシーは カーネル側にそのプロセスの属性として記録され、以降そのプロセスがシステムコールを発行するたびにカーネルが照合します。
ここで立場が逆転しています。アプリ層では「自分が自分を止めていた」のに対し、カーネル層では 止める側が、止められる側から手出しできない場所にいる。だから「自制」ではなく「強制」と呼べるわけです。実装は OS ごとに違いますが、性質は同じです。
- macOS (Seatbelt): カーネル内のフックでシステムコールごとにポリシーを照合し、許可されないアクセスを拒否する
- Linux (bubblewrap): namespace を使って、そのプロセスから見えるファイルシステムの景色自体を作り替える。拒否する以前に、
.envが存在しない世界を見せる
前者は「頼んでも断られる」、後者は「頼む対象がない」という違いですが、どちらも プロセスの外側で決着している 点が共通です。
ハードウェア層 : ここが「関与しない」になっているのは、単なる余白ではなく 限界の宣言 です。サンドボックスの強制力は「カーネルが正しく動いていること」に全面的に依存しています。カーネルの脆弱性を突かれて特権を取られた場合、判定者そのものが倒れるので、サンドボックスも一緒に倒れます。
つまりサンドボックスは、「うっかり cat .env される」を確実に防ぐ道具であって、「カーネルまで攻略してくる攻撃者」を想定した壁ではない、という位置づけです。
この「境界の上か下か」という視点は、後半で出てくる抜け穴を理解するときにも効いてきます。
どのプロセスに効いているのか
では、その強制は Claude Code のどのプロセスに効いているのでしょうか。中の構造はこうなっています。
ポイントは、Bash ツールは「起動時に明示的にサンドボックスでラップする」から拘束される という点です。裏を返せば、ラップされていないものには効きません。ここが後半の伏線になります。
なぜ Read ツールにはサンドボックスが効かないのか
「システムコールを止めるなら、Read ツールも止められるのでは?」と思うかもしれません。ここが理解の勘所です。
サンドボックスは プロセスに貼り付ける制約 です。プロセスが生成される瞬間に適用され、そのプロセスと、さらにその子プロセスに効きます。
一方、Read ツールは Claude Code 本体プロセスに組み込まれた機能 で、子プロセスを作りません。本体が自分で open() を呼んでファイルを読むだけです。つまり、サンドボックスを「貼り付ける対象となる新しいプロセス」がそもそも生まれない。だから、Read ツールを守りたければ本体ごとサンドボックスに入れるしかなく、それをやると本体の正常動作(設定の読み書きや通信)まで壊れてしまいます。
だから役割分担になっているのです。
- Read などの本体機能 →
permissions.deny(アプリの自制)で守る - Bash が起動する子プロセス → サンドボックス(OSの強制)で守る
両方揃えて初めて、正面玄関も裏口も塞がる わけです。
設定はこう書く ―― サンドボックスを有効にする
仕組みの話が続いたので、ここで一度、実際の設定を見ておきましょう。
まずは /sandbox で様子を見る
一番手軽なのは、セッション中にスラッシュコマンドを打つ方法です。
/sandbox
サンドボックスの設定パネルが開き、モードの選択(サンドボックス内のコマンドを自動承認するか、通常の権限確認を残すか)や、現在の設定内容を確認できます。ここで選んだ内容は、そのプロジェクトの .claude/settings.local.json に保存されます。
Linux で必要なパッケージが足りていない場合は Dependencies タブが出て、何が足りないかを教えてくれます。まずはこれを開いてみるのが早いです。
settings.json に書く
すべてのプロジェクトで有効にしたいなら、ユーザー設定 ~/.claude/settings.json に書きます。
{
"sandbox": {
"enabled": true
}
}
これだけです。以降、Bash ツールが起動するコマンドは、次の節で見る「fork して、隙間で設定して、exec」という流れでラップされて動きます。
なお、動く環境には条件があります。
- macOS: OS 内蔵の Seatbelt を使うので、追加インストールは不要
- Linux / WSL2:
bubblewrap(ファイルシステム隔離)とsocat(ネットワーク中継)が必要
sudo apt-get install bubblewrap socat # Ubuntu / Debian
ネイティブの Windows は非対応で、WSL2 の中で動かす必要があります。
重要 ―― 有効にするだけでは .env は読めたままです
そして、この記事のテーマにとって一番大事な注意点です。"enabled": true にしただけでは、.env の読み取りは止まりません。
デフォルトのポリシーは、読みと書きで非対称になっています。
- 書き込み: 作業ディレクトリ(と一時ディレクトリ)だけ許可
- 読み取り: マシン全体が許可(一部の拒否ディレクトリを除く)
つまり、デフォルトのサンドボックスが主に想定しているのは「作業ディレクトリの外を勝手に書き換えられること」と「知らないドメインに通信されること」の防止です。読み取りについてはかなり緩く、~/.aws/credentials や ~/.ssh/ すら読めます。
ですから、.env を守りたければ、読み取り拒否を明示的に書く必要があります。
{
"sandbox": {
"enabled": true,
"filesystem": {
"denyRead": ["./.env"]
},
"credentials": {
"files": [
{ "path": "~/.aws/credentials", "mode": "deny" },
{ "path": "~/.ssh", "mode": "deny" }
],
"envVars": [
{ "name": "GITHUB_TOKEN", "mode": "deny" }
]
}
}
}
filesystem.denyRead: サンドボックス内のプロセスからの読み取りを、OS レベルで拒否するcredentials.files: 同じことを「秘密情報」としてまとめて書ける枠("mode": "deny")credentials.envVars: サンドボックス実行前に、その環境変数を消す。ファイルを塞いでも、同じ秘密が環境変数に入っていては意味がないので、ここも大事です
パスの書き方には癖があります。sandbox.filesystem.* は一般的な慣習どおりで、/tmp/build が絶対パス、~/ がホーム、./ がプロジェクトルートです。ただし ./ がプロジェクトルートを指すのは プロジェクト設定(.claude/settings.json)に書いた場合だけ で、ユーザー設定に同じものを書くと ~/.claude からの相対になってしまいます。上の例のような ./.env は、プロジェクト側に置いてください。
2箇所に書くことになる
ここで、2章の permissions.deny と並べてみると、役割分担がはっきりします。
| 守りたい経路 | 書く場所 | 効いている層 |
|---|---|---|
| Read / Edit / Glob(本体の機能) | permissions.deny | アプリケーション層(自制) |
| Bash が起動する子プロセス | sandbox.filesystem.denyRead / sandbox.credentials | OS カーネル層(強制) |
同じ .env を守るために、2箇所に書くことになります。冗長に見えますが、これは重複ではありません。効いている層が違うので、片方だけでは片方の経路しか塞げないのです。さきほどの「両方揃えて初めて塞がる」を、設定ファイルの言葉に翻訳するとこうなる、というわけです。
サンドボックスの正体 ―― fork と exec
ここで、サンドボックスが具体的にどう「プロセスに制約を貼る」のかを見ておきます。実はこれは、特別な魔法ではなく Linux カーネルの基本的な仕組み をそのまま使っています。
Unix / Linux では、「プロセスを作る」と「プログラムを実行する」が 別々のシステムコール に分かれています。
fork() → 今のプロセスを複製する。新しいプロセスができる
exec() → 今のプロセスの「中身」を、別のプログラムに丸ごと入れ替える
exec() が独特です。新しいプロセスを作るのではなく、自分自身の中身を捨てて、別のプログラムに変身する のです。器(プロセス)はそのまま、中の人だけが入れ替わる。プロセス番号(PID)も変わりません。
一番わかりやすい確認方法があります。ターミナルで試してみてください。
echo "PID: $$"
exec bash # 新しい bash に変身する
echo "PID: $$" # ← PIDが変わっていない!
新しいシェルになったはずなのに PID が同じ。「変身」が起きている証拠です。
この仕組みが、サンドボックスの土台になっています。sandbox-exec(macOS の場合)のようなラッパーは、次の順番で動きます。
1. fork() で子プロセスを作る(この時点では sandbox-exec のコード)
2. カーネルにサンドボックスのポリシーを登録する
3. exec("bash") で自分自身を bash に変身させる
↑ ここで「sandbox-exec プロセス」は消え、同じ器が bash になる
4. 以降、その bash には制約が貼り付いた状態
つまり、「サンドボックスというプロセスが監視役として残る」わけではありません。fork で複製し、exec で目的のコマンドに変身するときに、制約という「属性」だけがそのプロセスに刻まれて残る のです。制約はカーネル側でそのプロセスに紐づいて記録されるので、exec で中身が入れ替わっても消えません。むしろ、消えずに引き継がれるからこそ、この仕組みが成立します。
なぜわざわざ「複製してから変身」という回りくどいことをするのか。理由は、fork と exec の間に「設定をするための隙間」ができる からです。この隙間で「サンドボックスのポリシーを登録する」「作業ディレクトリを変える」「環境変数を設定する」といった細工を、普通のコードとして自由に挟み込めます。env(環境変数を設定して実行)や nice(優先度を変えて実行)といったおなじみのコマンドも、実は全部この「隙間で細工してから exec する」という同じパターンで作られています。サンドボックスは、その一族の一員にすぎないのです。
そして、この制約は 子孫プロセスに継承されます。サンドボックス化された Bash が cat を呼べば、cat も同じ制約を受け継ぐ。だから「Bash 経由なら何を使っても読めない」が成立するわけです。
疑似コードで見る ―― サンドボックスが適用される瞬間
ここまでの話を、コードの形で一度に見てみましょう。ラッパーがやっていることは、だいたいこういう形です。
if (fork() == 0) { // 子プロセスの側
// ここは「まだ自分のコード」なので、好きなことができる
chdir("/tmp"); // 作業ディレクトリを変える
setuid(1000); // 権限を落とす
close(0); open("input.txt", ...); // 標準入力を差し替える
setenv("LANG", "C", 1); // 環境変数を設定
sandbox_init(profile, ...); // サンドボックスを適用
exec("/bin/ls", ...); // ここで初めて ls に化ける
}
fork() の戻り値が 0 になる側が子プロセスです。そして この if の中、exec() までの数行が、さきほど言った「隙間」 です。この時点でプロセスの中身はまだラッパー自身のコードなので、特別な仕掛けは何もいりません。普通の関数呼び出しとして準備ができます。
注目してほしいのは、並んでいる5行が 全部同じ性質の操作 だということです。chdir でも setuid でも setenv でも sandbox_init でも、設定している相手は共通で、「これから ls になる、いまの自分」 です。
つまり、サンドボックスの適用は「特殊な起動方法」ではありません。chdir() や setenv() と同じ列に、同じ資格で並んでいる ただの1行 です。3章の冒頭で「サンドボックスは env や nice の一族にすぎない」と書いたのは、こういう意味です。
そして最後の exec("/bin/ls")。ここでプロセスの中身が ls に入れ替わりますが、直前に設定したものは全部残ります。
- カレントディレクトリは
/tmp - UID は 1000
- 標準入力は
input.txt - 環境変数は
LANG=C - そして、サンドボックスのポリシーも
ls のコードは、自分がこれらを設定された覚えなどありません。それでも、生まれた瞬間からその条件下にいる。ここがサンドボックスの本質です。ls はサンドボックスの存在を知らないし、協力することも拒否することもできません。制約は「ls が守るべきルール」ではなく、「ls が置かれている環境」 だからです。
だからこそ、ls を cat に変えても、自作のバイナリに変えても意味がない。ここが、アプリケーション層の自制(permissions.deny)との決定的な違いです。
もうひとつ、順番が命 だという点も押さえておきたいところです。sandbox_init() は必ず exec() より前になければいけません。「exec した後に設定すればいい」は成立しません。exec の後には もう自分のコードが存在しない からです(中身は ls になっている)。設定するチャンスは fork と exec の間、この隙間しかないのです。
Claude Code が Bash ツールを起動するときも、やっていることはこれと同じです。逆に言えば ―― この隙間を通らずに起動されたプロセスには、当然、何の設定もされていない。これが次章の話につながります。
さらに残る抜け穴
「Read は permissions.deny、Bash はサンドボックス。これで完璧」と言いたいところですが、まだ抜け穴があります。
そのひとつが MCP サーバー です。MCP サーバーは、AI に外部ツール(ファイル操作、DB アクセスなど)を持たせるための仕組みで、Claude Code とは別のプロセスとして起動します。
「別プロセスなら、サンドボックスで守られるのでは?」と思うところですが、そうはなりません。もう一度、先ほどの図を見てください。
鍵は、図の一番上に書いてある 「Claude Code 本体はサンドボックス未適用」 という点です。
サンドボックスは「制約を持つプロセスから子へ継承される」仕組みでした。ところが本体自身は制約を持っていません。だから、本体が子プロセスを起動するとき、その起動処理がわざわざサンドボックスでラップしなければ、子は素のまま生まれます。
Bash ツールは「ラップする」ようにわざわざ実装されているので拘束されます。しかし MCP サーバーは 別のコードパスで起動される ため、そのパスがラップしていなければ、制約なしで動いてしまう。「子プロセスだから安全」ではなく、「ラップされた子プロセスだけが安全」なのです。継承元が無拘束である以上、包む処理を通らない経路はすべて素通りになります。
さきほどのレイヤー図の言葉で言い直すと、こうなります。システムコールの境界は すべてのプロセスに共通して存在している のに、そこで照合されるポリシーは プロセスごとに違う。MCP サーバーは、境界を越えていないわけではありません。境界は越えているが、そのプロセスには照らし合わせるルールが登録されていない ので、カーネルはそのまま通してしまうのです。
MCP サーバーは、ファイルにも DB にもネットワークにもアクセスできる強力な存在です。それが制約の外で動くというのは、.env にとっては立派な裏口になり得ます。
MCP サーバーを包む ―― @anthropic-ai/sandbox-runtime
では、この裏口は塞げないのでしょうか。塞げます。@anthropic-ai/sandbox-runtime という、Anthropic が公開しているサンドボックスツールを使います。
これは Claude Code の内蔵サンドボックスと同じ OS の仕組み(macOS なら Seatbelt、Linux なら bubblewrap)を、任意のプロセスに対して外から適用できるようにした単体パッケージ です。srt というコマンドを提供していて、公式のドキュメントでも「ローカル MCP サーバーを包むこと」が主要な想定用途として挙げられています。
npm install -g @anthropic-ai/sandbox-runtime
やっていることは、3章で見た sandbox-exec とまったく同じです。fork して、隙間でポリシーを登録して、exec で目的のコマンドに変身する。だから、包まれたプロセスの子孫にも制約が継承されます。
.mcp.json の command を差し替える
やり方はシンプルで、.mcp.json の command を srt に差し替えて、本来のコマンドを引数に回すだけです。
包む前:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem"]
}
}
}
包んだ後:
{
"mcpServers": {
"filesystem": {
"command": "srt",
"args": ["npx", "-y", "@modelcontextprotocol/server-filesystem"]
}
}
}
差分は「command を srt にして、元の command を args の先頭に押し込む」だけです。これで、この MCP サーバー(と、それが起動する子プロセス)はサンドボックスの中で動くようになります。MCP サーバー側に手を入れる必要はありません。サーバーは自分が包まれていることを知りませんが、それでも制約は効く ―― 3章の ls の話とまったく同じ構図です。
制約の内容は ~/.srt-settings.json に書きます。
{
"filesystem": {
"denyRead": ["./.env", "~/.ssh", "~/.aws"],
"allowWrite": ["."],
"denyWrite": ["./.env"]
},
"network": {
"allowedDomains": [],
"deniedDomains": []
}
}
srt --settings /path/to/srt-settings.json ... のように、サーバーごとに別の設定ファイルを渡すこともできます。
デフォルトの挙動には癖があるので、ここは押さえておいてください。
- 書き込み: デフォルトで 全部拒否。
allowWriteに書いたものだけ許可される - ネットワーク: デフォルトで 全部拒否。
allowedDomainsに書いたものだけ許可される - 読み取り: デフォルトで 全部許可。
denyReadで塞ぐ
内蔵サンドボックスと同じで、ここでも 読み取りだけは緩い という構図です。.env を守りたければ denyRead を明示的に書く必要があります。しかも srt では allowRead が denyRead より優先される ので、広い allowRead を書くと denyRead が打ち消されてしまいます(内蔵サンドボックスとは優先順位が逆なので、ここは要注意です)。
逆に、書き込みとネットワークはデフォルトで全部閉まっています。包んだ MCP サーバーが動かなくなったら、まず allowWrite と allowedDomains の不足を疑う のが正解です。
ただし注意点もあります。@anthropic-ai/sandbox-runtime は ベータのリサーチプレビュー という位置づけで、設定フォーマットは今後変わる可能性があると明記されています。運用ルールに組み込む場合は、そのつもりで扱ってください。
じゃあ、結局どうすればいいのか
ここまで、「経路を1つ塞いでも、別の経路が出てくる」というモグラ叩きを見てきました。Read を塞げば Bash、Bash を塞げば MCP……。塞ぐアプローチには、常に「列挙し忘れた経路」が残るという本質的な弱さがあります。
そこで発想を変えます。そもそも .env に本物の秘密を置かなければ、何経路から読まれても平気 です。守るべきものが最初からそこに無ければ、読まれても被害はありません。
理想的なのは、短命なアクセストークン を使う運用です。永続的な秘密鍵をファイルに置くのではなく、必要なときにだけ発行される、寿命の短いトークンで認証する。イメージとしては、こういう流れです。
1. 開発者が最初に CLI でログインする(人間の認証)
$ my-tool login
2. その権限をもとに、短命なアクセストークンをリクエストする
→ 数十分で失効するトークンが発行される
3. アプリはそのトークンを使って動く
→ ファイルには本物の秘密鍵が存在しない
→ 万一トークンが漏れても、すぐ失効するので被害が限定的
実際のコマンドで見るとこうなる
my-tool login は架空のコマンドですが、実際のクラウドでは、おなじみのコマンドがそのまま当てはまります。
AWS の場合
# 1. 人間の認証(ブラウザが開いて SSO でログインする)
aws sso login --profile dev
# 2〜3. あとは CLI / SDK が一時credentialを自動で取得して使う
aws s3 ls
~/.aws/credentials に永続的なアクセスキーを置く代わりに、aws sso login で得た権限をもとに、裏側で数時間で失効する一時credentialが発行されます。トークンは ~/.aws/sso/cache/ にキャッシュされるだけなので、期限が切れればそれ以上使えません。
Azure の場合
# 1. 人間の認証
az login
# 2. 必要なリソース向けの短命トークンを取得(有効期限は1時間程度)
az account get-access-token --resource https://vault.azure.net
アプリ側は DefaultAzureCredential を使えば、この CLI のログイン状態を自動で拾ってくれます。
Google Cloud の場合
# 1. 人間の認証
gcloud auth application-default login
# 2. 短命なアクセストークン(有効期限は1時間程度)
gcloud auth print-access-token
こちらも ADC(Application Default Credentials)という仕組みがあり、SDK が自動でトークンを取得してくれます。
どれも形はまったく同じです。「人間が一度ログインする」→「そこから短命なトークンが発行される」→「アプリはそれを使う」。そして共通して、.env に書くべき永続的な秘密がどこにも登場しません。
クラウドのマネージド ID(実行環境自身の身元でシークレットストアからトークンを取得する仕組み)や、OIDC ベースの一時credential(GitHub Actions からクラウドに、静的なアクセスキーを Secrets に置かずに認証する構成など)が、この考え方の実装にあたります。
秘密は「ファイルに書いて守る」のではなく、「そもそも書かず、必要な瞬間に短命なものを取りに行く」。これが、経路を1つずつ塞ぐ発想から抜け出す、根本的な解決策です。
とはいえ、理想はなかなか大変
ただ、正直に言うと、この理想形はすぐに実現できるものではありません。
- トークンを発行する仕組み(認証基盤、シークレットストア)を用意する必要がある
- 既存のアプリを「ファイルから読む」前提で書いている場合、その書き換えが要る
- ローカル開発、CI、本番で、それぞれトークンの取得経路を整える必要がある
規模の小さいプロジェクトや、とりあえず今動いているものに、いきなりこれを導入するのは腰が重い、というのが現実です。
なので、当面の現実的な立ち回りは、こうなります。
- 理想を目指しつつ、本物の秘密を短命トークンに寄せていく(長期的な方向性)
- それができるまでは、経路を1つずつ地道に塞ぐ
permissions.denyで Read / Edit / Glob をパス単位でブロック- サンドボックスを有効にし、
denyRead/credentialsで読み取りも明示的に塞ぐ(有効化だけでは読み取りは止まらない) - MCP サーバーは使うものを吟味し、
srt(@anthropic-ai/sandbox-runtime)で包む .envにはできる限りダミー値だけを置き、本物は最小限に
結局のところ、「守るべきものを、そこに置かない」が最強の対策 です。しかしそれが難しい間は、「経路をひとつずつ調べて防ぐ」しか、今のところ道はありません。モグラ叩きに聞こえるかもしれませんが、どの経路がどのレイヤーで守られるのか(アプリの自制なのか、OS の強制なのか)を理解していれば、叩き漏らしはずっと減らせます。
まとめ
コーディングエージェントを実用化させるためにはセキュリティは欠かせません。でもだからといって、コーディングエージェントならではの知識が必要なわけではなく、OS の基本的な仕組みと、プロセスの性質を理解していれば十分 です。今回の話は、Claude Code に限らず、コーディングエージェント全般に当てはまります。なので、やっぱりいくらAIを使って業務をしても、やっぱり基本が大事だなと思います。











