エージェントに任せれば速い。それは間違いない。ただ、その速さと引き換えに、自分のAPIキーや本番の接続情報を丸ごと握らせている——ふとそう気づくと、手が止まる。便利さに慣れた頃に限って、この居心地の悪さはやってくる。
でも、ここには思い込みが一つ混じっています。エージェントを動かすのに、鍵を直接渡す必要はないのです。渡す代わりに、触れる範囲そのものを絞る。ネットワーク、秘匿値、信用するコマンド——この3つを最小限に切り詰めておけば、鍵を握らせなくても仕事は回ります。
この記事は、「AIに何かを任せたいが、鍵は渡したくない」を成立させるための、権限設計の線引きの記録です。なお、動くツールを作ったあとに認証・権限をどう見直すかを非技術者向けに整理した記事(AIで社内ツールは作れた。でも、その認証とアクセス権限は大丈夫ですか)もあります。本記事はその技術寄りの続きにあたります。
この記事で扱うこと
- ネットワークの送信先をOSレベルで「デフォルト全拒否」にする考え方
- 秘匿値は「読ませない」より「出力で止める」、そして本番の鍵は最初から渡さない
- 信用するコマンドをパス単位で絞る(interpreterごと除外しない)
前提
- Claude Code を業務で日常的に使っている
settings.jsonの編集に抵抗がない- macOS(OS の隔離機構として Seatbelt を利用)
絞るのは次の3つの範囲です。以降の章で、順に実装を見ていきます。
| 絞る対象 | やること | 手段 |
|---|---|---|
| ネットワーク | 送信先をデフォルト全拒否にし、必要な先だけ開ける | sandbox の allowlist + failIfUnavailable / allowUnsandboxedCommands |
| 秘匿値 | 「読ませない」より出力で止める。本番の鍵は最初から渡さない | 出力マスク(フック)+ 本番参照は実行前フックで確認 |
| 信用範囲 | 信用するコマンドをパス単位で絞る(interpreter ごと除外しない) | excludedCommands をパス起点で列挙 |
全体像はこうです。鍵を渡す代わりに、エージェントが触れる範囲そのものを閉じていきます。
送信先を「デフォルト全拒否」から始める
鍵を渡さない設計は、ネットワークを閉じるところから始まります。順番が逆だと意味がありません。いくら鍵を隠しても、送信先が開いていれば、拾われた情報はそのまま外へ出ていくからです。
素朴な対策は「危険なコマンドを拒否リストに入れる」です。実際、送信を担いがちなコマンドは名指しで止めています。
{
"permissions": {
"deny": [
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(ssh:*)",
"Bash(scp:*)",
"Bash(nc :*)"
]
}
}
ただ、これだけでは穴が残ります。コマンド名での照合は、名前を持たない経路——パッケージの postinstall スクリプト、自前スクリプトが内部で呼ぶ HTTP クライアント、コンパイル済みのバイナリ——を素通しします。名指しで止める方式は、名指しできる相手にしか効きません。
そこで、名前ではなく OS レベルで「デフォルト全拒否」をかけ、通ってよい送信先だけを allowlist で開けます。
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false,
"network": {
"allowedDomains": ["github.com", "registry.npmjs.org", "pypi.org"]
}
}
}
allowedDomains に並べるのは、ツールチェーンが実際に叩く先だけ。それ以外は初回に確認が走り、許可すれば記憶されます。肝は failIfUnavailable と allowUnsandboxedCommands の組み合わせです。前者は sandbox の初期化に失敗したとき隔離なしで黙って続行する(=送信先が再び全開になる)のを禁じ、後者はモデルが sandbox を一時的に無効化してすり抜けるのを禁じます。この2つを両方ハードゲートにしておかないと、「いつの間にか守りが外れていた」が起きる。コマンド名の deny はあくまで二枚目の壁で、一枚目はこの OS レベルの隔離です。
秘匿値は「読ませない」より「出力で止める」、本番の鍵は渡さない
送信先を閉じたら、次は秘匿値そのものの扱いです。ここで二段構えを取ります。
一段目は「漏らさない」。素直に考えると「.env を読ませなければいい」となりますが、これは罠でした。読み取りを止めると、ビルドやスクリプトが正規に環境変数を読み込む処理まで一緒に壊れます。だから読み取り禁止に置くのは、本当に触る必要のない認証情報のディレクトリだけに絞ります。
{
"sandbox": {
"filesystem": {
"denyRead": ["~/.aws/**", "~/.ssh/**", "~/.gnupg/**"]
}
}
}
.env はここに入れません。代わりに、ツールの実行結果が文脈に流れ込む直前にフックを挟み、出力テキスト中の秘匿パターンを [REDACTED:...] に置き換えます。読み込みは妨げないので正規の処理は壊れず、しかし値は会話に残らない。マスク対象で効くのは、URL に埋め込まれた認証情報(scheme://user:pass@host の形)と、名前に KEY・TOKEN・SECRET・PASSWORD を含む環境変数形式の値です。このマスク設計の詳しい検証は、別記事(Claude Code のセキュリティ設定を実運用で固める)にまとめています。
二段目が、この記事の本題である「本番の鍵は最初から渡さない」です。エージェントには test や staging の資格情報だけを持たせ、本番の鍵は視界に入れない。とはいえ、うっかり本番を参照するコマンドが生成されることはあります。そこを実行前フックで止めます。
# 本番 credential を参照するコマンドを実行前に検知する
echo '{"tool_name":"Bash","tool_input":{"command":"echo $PROD_API_KEY"}}' \
| bash ~/.claude/hooks/pretool-bash-credential-guard.sh
このフックは、本番を示す参照——$PROD_/$PRODUCTION_/$LIVE_ で始まる環境変数、.env.production/.env.prod ファイルの読み込み、aws --profile に prod を含むプロファイル——を拾うと permissionDecision: "ask" を返します。完全拒否ではなく確認に倒すので、必要なら人が明示的に許可して進めます。
大事なのは、ここが雑に広げると暴発する点です。PROD を含むだけで拾うと、echo $PRODUCER(ただの変数名)も ls products/ も巻き込まれる。そこでフックは境界を厳密にしています。環境変数は PROD の直後に _ を必須にして $PRODUCER を外し、ファイルは .env.staging や .env.test を当てず、プロファイルも staging や default は素通しする。「本番だけを止め、似て非なるものは通す」——この精度があって初めて、本番の鍵を日常的に遠ざけられます。
信用する範囲はパス単位で絞る
送信先を閉じると、副作用が出ます。OS の隔離下では、GitHub 操作を伴う処理が macOS の keychain にアクセスできず、TLS 検証に失敗して黙り込む。動かすには、この手の処理を隔離の外に出す必要があります。
ここでやってはいけないのが、「実行を司る interpreter ごと除外する」という雑な対処です。「python3 で始まるコマンドは全部除外」「bash で始まるコマンドは全部除外」とやれば確かに動きますが、それでは任意のスクリプトが隔離の外に出てしまい、最初に閉じたはずの送信先が意味を失います。守りを取り戻したつもりで、別の場所に穴を開けている。
採るのは、実行経路の「パス」で除外する方針です。
{
"sandbox": {
"excludedCommands": [
"gh:*",
"~/.claude/skills/*",
"bash ~/.claude/skills/*",
"python3 ~/.claude/skills/*"
]
}
}
同じ場所に置かれたコマンドだけを、起動の形(直接実行・bash 経由・python3 経由のいずれでも)に依存せず除外します。「この言語で書かれたものは全部信用する」ではなく、「この場所に置かれたものだけ信用する」。信用の単位を場所に紐づけることで、任意のスクリプトに穴が広がるのを防ぎます。 絶対パスを避けてホーム起点で書くのは、設定を複数マシンで同期したときにユーザー名が埋まり込んで壊れないためです。
なお、GitHub クライアントを隔離の外に出しても、その認証情報を保管するディレクトリは読み取り禁止のままにしてあります。「この処理だけ隔離の外で動かす」と「その秘密を他のプロセスから守る」は両立する。外に出したからといって、全部を無防備にする必要はありません。
動作確認
本番の global 設定に反映する前に、一時的な local 設定で有効化して以下を確かめると安全です。
- 許可していない送信先が実際にブロックされるか
- 秘匿情報を含む出力が
[REDACTED]に置き換わるか - 本番を参照するコマンド(
$PROD_...や.env.production)で確認プロンプトが出て、staging やproducts/では出ないか - パスで除外したツールが動き、なおかつ interpreter 全体は除外されていないか
allowUnsandboxedCommands を false にしている以上、除外し忘れたツールは「確認が出る」ではなく「ハードに失敗する」挙動になります。普段使うツールは事前に洗い出しておくと、運用開始後に慌てずに済みます。
まとめ
「鍵を渡さずにエージェントを動かす」とは、根性論ではなく、3つの範囲を最小に絞る作業でした。
送信先は OS レベルでデフォルト全拒否にして、通ってよいドメインだけ開ける。秘匿値は読み取り禁止で塞ぐのではなく出力でマスクし、本番の鍵はそもそも渡さず、参照しかけたら確認に倒す。信用するコマンドは interpreter 単位ではなくパス単位で、範囲を狭く具体的に切り出す。
守りを固めるほど正当な作業が壊れる、という緊張はここでも同じです。雑に全部を禁止すれば作業が止まり、雑に全部を渡せば鍵が漏れる。その間で、渡さずに済む一点を探すことが、エージェントに安心して仕事を任せるということでした。



