毎日決まった時刻に Claude Code を無人で走らせて、ビルドとテストをサンドボックス内で回しています。ある日ふと、サンドボックスの読み取り範囲を /sandbox の Config タブで眺めていて、手が止まりました。書き込みは作業ディレクトリと一時領域に絞られているのに、読み取りはマシン全体に開いたままで、その中に ~/.aws/credentials も ~/.ssh/ も含まれていたのです。エージェントが流し込む npm test の子プロセスが、原理的にはこれらを読めてしまう状態でした。
サンドボックスは「コードを安全に動かす」ための仕組みだと素朴に思い込んでいたのですが、既定の読み取りポリシーは想像よりずっと広いものでした。v2.1.187 で追加された sandbox.credentials を使って、この読み取りの露出面をOS強制レベルで絞った実作業を、個人開発で複数サイトを無人運用している私自身の経験として共有します。何が止まって何が止まらないのかを正確に押さえないと、安心したつもりで穴を残すので、その線引きを中心に書きます。
既定の読み取りはなぜマシン全体に開いているのか
サンドボックスの既定の振る舞いは、書き込みと読み取りで非対称です。書き込みは作業ディレクトリとそのサブディレクトリ、それに $TMPDIR が指すセッション一時領域だけに絞られます。一方、読み取りは一部の拒否ディレクトリを除いて「マシン全体」に開いています。
この非対称には理由があります。ビルドやテストは、ホームディレクトリ下のツールキャッシュ、システムの共有ライブラリ、グローバルな設定ファイルなど、作業ディレクトリの外を読めないと動かないことが多いからです。読み取りを作業ディレクトリだけに絞ると、ほとんどのツールチェーンが壊れます。だから既定は読み取りを広く開け、書き込みだけを厳しく閉じる設計になっています。
問題は、その「広く開いた読み取り」の中に認証情報が含まれている点です。公式ドキュメントも明記していますが、既定の読み取りポリシーは ~/.aws/credentials や ~/.ssh/ の読み取りを依然として許します。無人で回すパイプラインでは、ここが地味に効いてきます。エージェントが生成・実行するコマンドは毎回同じとは限らず、何らかの拍子に認証ファイルを読むコマンドが流れたとき、サンドボックス自体はそれを止めません。
sandbox.credentials が止めるもの、止めないもの
sandbox.credentials は、サンドボックス内のコマンドがアクセスしてはいけない認証ファイルと環境変数を宣言する設定です。動きは2系統に分かれます。
ひとつは files。ここに挙げたパスは、サンドボックス内での読み取りが拒否されます。これは filesystem.denyRead がかけるのと同じ読み取りブロックです。もうひとつは envVars。ここに挙げた環境変数は、サンドボックス化された各コマンドの実行前に unset されます。認証ファイルの読み取り拒否と、秘密の環境変数の除去を、ひとつの credentials ブロックにまとめて書けるのが要点です。
逆に、止まらないものを正確に把握しておく必要があります。次の表は、sandbox.credentials の効果範囲を整理したものです。
| 対象 | sandbox.credentials の効果 |
| サンドボックス化された Bash コマンドとその子プロセス | files は読み取り拒否、envVars は実行前に unset |
| Read / Edit / Write の組み込みファイルツール | 対象外(サンドボックスではなく権限システムで制御) |
| MCP サーバー経由のアクセス | 対象外 |
| ネットワーク経由の流出 | 対象外(ドメイン許可リストとプロキシで別途制御) |
| サンドボックスを無効化して動くコマンド | 対象外(通常の権限フローに従う) |
つまり sandbox.credentials は「サンドボックス化された Bash の読み取り経路」だけを塞ぐ部品です。組み込みの Read ツールで認証ファイルを開かれることや、許可した広いドメインへ中身を送られることは、別のレイヤーで守る必要があります。ここを混同すると、片側だけ閉じて安心してしまう、というのが無人運用でハマりやすい落とし穴です。本番運用では、この線引きを取り違えると検知も難しくなります。私の場合は、まず「サンドボックスのBash読み取り」という一枚だけを sandbox.credentials で固める、と役割を割り切って考えています。秘匿情報のハンドリングを通しで設計したい場合は、トラスト境界の切り分けを扱ったClaude Code のシークレット管理とトラスト境界設計も併せて読むと、sandbox.credentials がどのレイヤーの部品なのかが立体的に見えてきます。
Before / After:実際の設定
まず、サンドボックスは有効だが認証情報の保護をしていない状態です。読み取りは既定のまま、つまりマシン全体に開いています。
{
"sandbox": {
"enabled": true,
"filesystem": {
"allowWrite": ["~/.npm", "/tmp/build"]
}
}
}
この状態だと、サンドボックス内で次のコマンドが通ってしまいます。
# Before: サンドボックス内なのに読めてしまう
cat ~/.aws/credentials
echo "$GITHUB_TOKEN"
ここに credentials ブロックを足したのが After です。AWS 認証ファイルと SSH ディレクトリの読み取りを拒否し、GITHUB_TOKEN と NPM_TOKEN をサンドボックス化コマンドの環境から外します。
{
"sandbox": {
"enabled": true,
"filesystem": {
"allowWrite": ["~/.npm", "/tmp/build"]
},
"credentials": {
"files": [
{ "path": "~/.aws/credentials", "mode": "deny" },
{ "path": "~/.ssh", "mode": "deny" }
],
"envVars": [
{ "name": "GITHUB_TOKEN", "mode": "deny" },
{ "name": "NPM_TOKEN", "mode": "deny" }
]
}
}
}
各エントリの "mode": "deny" は、現時点で唯一サポートされる値です。mode を明示するスキーマにしてあるのは、将来のモード追加に備えて前方互換を保つためです。files のパスは sandbox.filesystem.* と同じプレフィックス規約(~/ はホーム、/ は絶対、接頭辞なしは設定の置き場所に対する相対)に従います。
ひとつ注意したいのは、組み込みの拒否リストは存在しないことです。挙げたファイルと変数だけが制限されます。~/.aws/credentials を書いても ~/.config/gcloud は自動では守られません。自分の環境にある認証情報を棚卸しして、明示的に列挙する作業が要ります。
envVars の unset と環境スクラブの使い分け
環境変数まわりには、似て非なる2つの仕組みがあります。混同しやすいので、判断軸を分けておきます。
sandbox.credentials.envVars は、サンドボックス化された Bash コマンドの環境からだけ、指定した変数を unset します。普段の対話的な操作や、サンドボックスの外で動くコマンドには影響しません。「サンドボックスに渡したくない特定の秘密」をピンポイントで外すのに向いています。
一方、サンドボックスの有無にかかわらず、Anthropic とクラウドプロバイダの認証情報をすべての子プロセスから剥がしたいなら、環境変数 CLAUDE_CODE_SUBPROCESS_ENV_SCRUB を使います。こちらは適用範囲が広く、サンドボックス化されていない子プロセスにも効きます。
判断はこう整理しています。自分のアプリ固有のトークン(GITHUB_TOKEN や NPM_TOKEN のような、列挙したい個別の秘密)は sandbox.credentials.envVars で外す。Anthropic・クラウド系の一般的な認証情報を広く剥がしたいときは CLAUDE_CODE_SUBPROCESS_ENV_SCRUB を併用する。両者は排他ではなく、重ねて使う前提の部品です。
denyRead との違い:なぜ別ブロックに分けるのか
files の読み取り拒否は、機能としては sandbox.filesystem.denyRead と同じ効果です。では、なぜ credentials という別ブロックに分けるのでしょうか。
実務上の理由は、可読性と意図の明確さです。filesystem.denyRead は一般的なファイルシステム規則の置き場で、ビルド成果物の保護やキャッシュの隠蔽など、用途が混在します。そこに認証情報の保護を混ぜ込むと、後から設定を読んだとき「このパスは何の意図で塞いだのか」が分かりにくくなります。credentials ブロックは、ファイルの読み取り拒否と環境変数の unset を「秘密を守る」という単一の意図のもとにまとめます。設定が育っても、認証情報の保護だけを一箇所で点検できます。
挙動面での共通点も押さえておきます。credentials の各スコープ(ユーザー・プロジェクト・ローカル・managed)のエントリはマージされます。そしてモードは deny のみなので、どのスコープも制限を「追加」できますが、「解除」はできません。つまり、いったん組織やプロジェクトで塞いだ認証情報を、より下位のスコープが上書きで開けることはありません。これは秘密の保護という用途にとって、安全側に倒れた望ましい性質です。
設定が効いているかを検証する
設定を書いたら、効いていることを必ず確かめます。サンドボックス内で認証ファイルと環境変数に触れて、拒否されることを目で見ます。検証は2方向で行います。
読み取り拒否を確かめる
まず、読み取り拒否の確認です。サンドボックス化された Bash で認証ファイルを開こうとして、失敗することを見ます。
# After: いずれもサンドボックス内では失敗するのが正
cat ~/.aws/credentials # 読み取り拒否で失敗する
ls ~/.ssh # ディレクトリ読み取りが塞がれている
環境変数の unset を確かめる
次に、環境変数の unset 確認です。envVars に挙げた変数が、サンドボックス化コマンドの実行時に空であることを確認します。
# 出力が空(未設定)になっていれば unset が効いている
printf 'GITHUB_TOKEN=[%s]\n' "${GITHUB_TOKEN:-}"
printf 'NPM_TOKEN=[%s]\n' "${NPM_TOKEN:-}"
ここで大事なのは、検証コマンドがサンドボックス内で動いていることです。サンドボックスを無効化して動いたコマンド(dangerouslyDisableSandbox でフォールバックしたもの)は通常の権限フローに従うため、credentials の制限がかかりません。検証結果を読むときは「いま走ったコマンドはサンドボックス内だったか」を必ず確かめます。/sandbox の Config タブで解決後の設定を確認し、想定したパスと変数が反映されているかも併せて見ておくと確実です。
回帰を見張る番人を置く
無人運用では、この検証を一度きりにせず、回帰を見張る最小の番人を置いておくことを推奨します。設定ファイルから credentials.files と envVars の件数を数え、想定値を下回ったら気づける、という程度の軽いチェックで十分役に立ちます。
#!/usr/bin/env bash
# credentials の保護件数が想定を割っていないか見張る最小の番人
set -euo pipefail
SETTINGS="${1:-.claude/settings.json}"
EXPECT_FILES="${2:-2}"
EXPECT_ENVS="${3:-2}"
files=$(grep -c '"mode": *"deny"' "$SETTINGS" || true)
# files と envVars は同じ "mode":"deny" を持つため、ブロック単位で数える
n_files=$(python3 -c '
import json,sys
c=json.load(open(sys.argv[1]))["sandbox"]["credentials"]
print(len(c.get("files",[])), len(c.get("envVars",[])))
' "$SETTINGS")
read -r got_files got_envs <<< "$n_files"
if [ "$got_files" -lt "$EXPECT_FILES" ] || [ "$got_envs" -lt "$EXPECT_ENVS" ]; then
echo "credentials 保護が縮小: files=$got_files(>=${EXPECT_FILES}) envVars=$got_envs(>=${EXPECT_ENVS})" >&2
exit 1
fi
echo "OK: files=$got_files envVars=$got_envs"
設定はマージされて増える方向なので、件数が想定を下回ったら、どこかのスコープで意図せず構成が崩れたサインです。番人が落ちたら設定を見直します。
組織での強制と、設定が触れない限界
個人運用を越えてチームや組織で効かせたい場合は、managed 設定で配ります。サンドボックスを必須にし、依存欠如時に起動を止め、サンドボックス外への退避を禁じたうえで、credentials に ~/.aws や ~/.ssh などのディレクトリと秘密の環境変数を加えるのが定石です。既定の読み取りがこれらを開けたままにする以上、組織配布の設定にこそ credentials を入れる価値があります。
読み取りパスを組織の許可値だけに固定したいなら、managed 設定で allowManagedReadPathsOnly を true にします。ユーザー・プロジェクト・ローカルの allowRead が無視され、開発者が読み取りを勝手に広げられなくなります。credentials がモード deny のみで「解除不可」なのと合わせると、下位スコープからの抜け道を塞ぎやすくなります。
最後に、sandbox.credentials が触れない限界を正直に書いておきます。これは読み取りと環境変数の露出面を絞る部品であって、ネットワーク経由の流出を止めるものではありません。広いドメイン(たとえば汎用的なホスト)を許可リストに入れていれば、サンドボックス内のコードがそこへデータを送る経路は残ります。読み取りを塞いでも、ネットワーク側の許可が緩ければ全体としての防御は完成しません。フィルタリングはホスト名ベースで TLS の中身までは検査しない設計である点も踏まえ、ファイルシステムとネットワークの両側を揃えて見直すのが要点です。
無人で回すパイプラインを持っているなら、次の一歩は具体的です。/sandbox の Config タブで自分のサンドボックスの読み取り範囲を一度確認し、そこに含まれている認証ファイルと、環境に乗っているトークンを棚卸ししてください。sandbox.credentials に列挙して、サンドボックス内の cat と echo で拒否を目視する。ここまでやって初めて、サンドボックスは「コードを動かせても秘密は読ませない」状態に近づきます。