Sandbox Allowlist Recipe (Firecrawl / Web Scraping 用)
claude-code-harness を install した他プロジェクトで Firecrawl・テックブログ取得・外部 API 呼び出しが HTTP/2 403 / x-deny-reason: host_not_allowed で塞がれる時の解決レシピ。
TL;DR: CC sandbox は default で allowlist 空 = 全 deny。ユーザー global の
~/.claude/settings.jsonにsandbox.network.allowedDomainsを追加するのが正規ルート。AI 経由で書き換えは self-audit guard で deny されるため、ユーザー手動編集。
症状
外部プロジェクトで Firecrawl CLI / WebFetch / curl が 403 / connection refused になる。Bash subprocess のログに以下が出る:
HTTP/2 403
x-deny-reason: host_not_allowedまたは
curl: (6) Could not resolve host: api.firecrawl.dev原因
Claude Code sandbox(macOS Seatbelt / Linux bubblewrap)は allowlist default。~/.claude/settings.json に sandbox.network.allowedDomains が無い = どのホストへも外向き通信できない。
Firecrawl plugin の SKILL.md を確認すると allowed-tools: Bash(firecrawl *)。つまり Firecrawl CLI は Bash subprocess として走り、sandbox の影響を直接受ける(MCP server ではない)。
Migration: runtime floor secret allow は plan-time 事前確認へ
HARNESS_RUNTIME_FLOOR_SECRET_ALLOW のような広域・常設の secret-read allow 宣言は、新規運用では避ける。 代わりに /harness-plan create の計画確定時に 事前確認セクションを出し、task scope ごとに必要な secret-read path / 外部送信 / 破壊的操作を一括承認する。
承認結果は .claude/state/plan-preapprovals.json(plan-preapproval.v2)に保存する。v2 は phase/task scope、expires_at、max_uses、uses を持つ。v1 は既存記録の読み取り互換に限る。 /harness-work / /breezing は task 開始時に .claude/state/active-task.json を書く。scripts/plan-preapproval.sh apply-secret-allow "$PROJECT_ROOT" は現在の phase/task と一致する承認済み secret-read path だけを project config の runtimefloor.secretAllow へ反映する。 記録に無い未計画の secret-read / 外部送信は従来どおり runtime floor / ask で停止するため、安全網は狭めない。
解決: ~/.claude/settings.json に sandbox 設定を merge
重要: ~/.claude/settings.json に 既存の sandbox キーがあるかどうか で 2 ケース分岐する。誤って既存 sandbox を上書きすると、failIfUnavailable / filesystem.denyRead / network.deniedDomains などの既存 guardrail が消える。
Step 0: 既存 sandbox の有無を確認
jq 'has("sandbox")' ~/.claude/settings.json
# false → Case A (新規追加)
# true → Case B (内側 merge)Case A: 既存に sandbox キーが無い場合 (新規追加)
既存の permissions / hooks / enabledPlugins / mcpServers 等と 同じ階層 (top-level) に sandbox キーを 1 つ追加する。既存キーは touch しない:
{
"permissions": { /* 既存維持 */ },
"hooks": { /* 既存維持 */ },
"enabledPlugins": { /* 既存維持 */ },
"mcpServers": { /* 既存維持 */ },
/* ... 他の既存 top-level keys も全て維持 ... */
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true,
"excludedCommands": [
"docker", "docker-compose", "watchman",
"systemctl", "launchctl", "brew services"
],
"network": {
"allowedDomains": [
"github.com", "api.github.com", "raw.githubusercontent.com",
"codeload.github.com", "objects.githubusercontent.com",
"registry.npmjs.org", "api.anthropic.com",
"pypi.org", "files.pythonhosted.org",
"proxy.golang.org", "sum.golang.org",
"crates.io", "static.crates.io", "rubygems.org",
"api.firecrawl.dev", "firecrawl.dev",
"techblog.zozo.com", "note.com", "assets.st-note.com",
"zenn.dev", "qiita.com", "dev.to", "medium.com",
"cdn-ak.f.st-hatena.com",
"engineering.dena.com", "developers.cyberagent.co.jp",
"tech.uzabase.com", "engineer.crowdworks.jp", "tech.smarthr.jp"
],
"deniedDomains": [
"169.254.169.254", "metadata.google.internal", "metadata.azure.com",
"pastebin.com", "transfer.sh", "0x0.st",
"paste.ee", "termbin.com", "ix.io"
]
}
}
}Case B: 既存に sandbox キーがある場合 (内側 merge)
既存の sandbox.failIfUnavailable / sandbox.filesystem / sandbox.network.deniedDomains などを 保持したまま、内側にフィールドを追加 / 統合する。sandbox ブロック全体の置換は禁止 (既存 guardrail を破壊する)。
merge ルール:
| フィールド | 操作 | 注意 |
|---|---|---|
sandbox.enabled | true に設定 | 既に true なら維持 |
sandbox.autoAllowBashIfSandboxed | true に設定 | 新規追加 |
sandbox.failIfUnavailable | 既存維持 | 触らない |
sandbox.excludedCommands | 配列なら union (重複排除して結合)、無ければ新規追加 | 既存項目を消さない |
sandbox.network.allowedDomains | 既存配列 + 本 recipe の 29 個を union | 既存ホストを消さない |
sandbox.network.deniedDomains | 既存配列 + 本 recipe の 9 個を union | 既存遮断ホストを残す |
sandbox.filesystem | 既存維持 | touch 禁止 (denyRead/allowRead 等が消える) |
自動 merge する jq one-liner (Case A / B 両対応)
エディタでの手動 merge は重複と guardrail 消去のリスクが高い。以下の jq one-liner は両 case 安全:
SETTINGS=~/.claude/settings.json
# 1. 元のファイル mode を保存 (token を含むため 600 等で保護されているケースに対応)
# cross-platform stat: Linux GNU stat -c を先に試し、macOS BSD stat -f に fallback
# (順序重要: BSD stat -f は Linux では filesystem-status flag として誤動作する)
MODE=$(stat -c '%a' "$SETTINGS" 2>/dev/null || stat -f '%Lp' "$SETTINGS")
# 2. backup (cp -p で mode/ownership を保持)
cp -p "$SETTINGS" "${SETTINGS}.bak.$(date +%Y%m%d-%H%M%S)"
# 3. merge (既存 sandbox.filesystem / failIfUnavailable は保持、配列は union)
jq '
.sandbox.enabled = true |
.sandbox.autoAllowBashIfSandboxed = true |
.sandbox.excludedCommands = (((.sandbox.excludedCommands // []) + [
"docker", "docker-compose", "watchman",
"systemctl", "launchctl", "brew services"
]) | unique) |
.sandbox.network.allowedDomains = (((.sandbox.network.allowedDomains // []) + [
"github.com", "api.github.com", "raw.githubusercontent.com",
"codeload.github.com", "objects.githubusercontent.com",
"registry.npmjs.org", "api.anthropic.com",
"pypi.org", "files.pythonhosted.org",
"proxy.golang.org", "sum.golang.org",
"crates.io", "static.crates.io", "rubygems.org",
"api.firecrawl.dev", "firecrawl.dev",
"techblog.zozo.com", "note.com", "assets.st-note.com",
"zenn.dev", "qiita.com", "dev.to", "medium.com",
"cdn-ak.f.st-hatena.com",
"engineering.dena.com", "developers.cyberagent.co.jp",
"tech.uzabase.com", "engineer.crowdworks.jp", "tech.smarthr.jp"
]) | unique) |
.sandbox.network.deniedDomains = (((.sandbox.network.deniedDomains // []) + [
"169.254.169.254", "metadata.google.internal", "metadata.azure.com",
"pastebin.com", "transfer.sh", "0x0.st",
"paste.ee", "termbin.com", "ix.io"
]) | unique)
' "$SETTINGS" > "${SETTINGS}.tmp" \
&& chmod "$MODE" "${SETTINGS}.tmp" \
&& mv "${SETTINGS}.tmp" "$SETTINGS"
# 4. mode が保持されたか念のため確認 (元の mode と一致するはず)
# 順序は MODE 取得と同じ: Linux GNU stat -c → macOS BSD stat -f fallback
stat -c '%a' "$SETTINGS" 2>/dev/null || stat -f '%Lp' "$SETTINGS"なぜ
chmod "$MODE"が必要か:>redirect +mvパターンは tmp ファイルを umask (一般に022→ 644) で作成するため、元の~/.claude/settings.jsonが600(token / secret を含むため強い permission で保護) だった場合、merge 後に read access が広がる security regression が起きる。chmod "$MODE"で元の mode を明示復元すれば、token を含むファイルでも安全。
AI からこの jq を実行できない理由:
~/.claude/settings.jsonは AI による self-tampering 防止対象 (Edit/Write(.claude/settings*)deny + auto mode classifier が Bash 経由の迂回も block)。ユーザー自身がターミナルで実行する前提のレシピ。
検証
# JSON 構文
jq -e '.' ~/.claude/settings.json > /dev/null && echo "VALID JSON"
# allowedDomains の件数
# Case A (既存 sandbox 無し): ちょうど 29
# Case B (既存 sandbox あり): 29 以上 (既存と union したので 29 + 既存独自分)
jq '.sandbox.network.allowedDomains | length' ~/.claude/settings.json
# deniedDomains の件数
# Case A: ちょうど 9 / Case B: 9 以上
jq '.sandbox.network.deniedDomains | length' ~/.claude/settings.json
# 必須ホストが含まれているか (Case A / B 共通の最低条件)
# 注意: jq array `contains` は string substring match なので "www.firecrawl.dev" が
# "firecrawl.dev" を含むと誤判定する。exact match のため any(. == "...") を使う
# (any() は ! を含まないため zsh history expansion との衝突も無い)
jq -e '
(.sandbox.network.allowedDomains | any(. == "api.firecrawl.dev")) and
(.sandbox.network.allowedDomains | any(. == "firecrawl.dev")) and
(.sandbox.network.deniedDomains | any(. == "169.254.169.254")) and
(.sandbox.network.deniedDomains | any(. == "pastebin.com"))
' ~/.claude/settings.json && echo "REQUIRED HOSTS PRESENT"
# Case B 限定: 既存 filesystem セクションが破壊されていないか
jq '.sandbox.filesystem // "no filesystem section (Case A)"' ~/.claude/settings.json
# 既存の enabledPlugins が壊れていないか (Case A / B 共通)
jq '.enabledPlugins | length' ~/.claude/settings.json
# → 既存件数を維持CC 再起動
sandbox 設定は session start 時にのみ読まれる。merge 後は CC を完全再起動 (cmd+Q → 再起動) で initialize される。
構成の意図
3 階層で先回り許可する設計:
| 階層 | ドメイン | 用途 |
|---|---|---|
| 開発コア (14) | github.com / api.github.com / raw.githubusercontent.com / codeload.github.com / objects.githubusercontent.com / registry.npmjs.org / api.anthropic.com / pypi.org / files.pythonhosted.org / proxy.golang.org / sum.golang.org / crates.io / static.crates.io / rubygems.org | npm install / pip install / go mod / cargo / git clone |
| Firecrawl (2) | api.firecrawl.dev / firecrawl.dev | Firecrawl API endpoint |
| スクレイプ対象 (13) | techblog.zozo.com / note.com / assets.st-note.com / zenn.dev / qiita.com / dev.to / medium.com / cdn-ak.f.st-hatena.com / engineering.dena.com / developers.cyberagent.co.jp / tech.uzabase.com / engineer.crowdworks.jp / tech.smarthr.jp | 日本/英語のテックブログ・記事スクレイプ |
deniedDomains 9 個(クラウド metadata endpoint と pastebin 系)は SSRF + 情報流出経路の遮断として維持。allowedDomains で許可してもこちらが優先で deny される。
各 sandbox オプションの意味
| キー | 値 | 意味 |
|---|---|---|
enabled | true | CC 起動時から sandbox を ON にする。/sandbox コマンドでの手動起動が不要 |
autoAllowBashIfSandboxed | true | sandbox に閉じ込められた Bash subprocess は permission ダイアログ無しで自動許可。autonomous セッションが止まらない |
excludedCommands | docker / docker-compose / watchman / systemctl / launchctl / brew services | sandbox 内で動かせない OS 系コマンドは sandbox 外で実行に逃がす |
network.allowedDomains | 29 個 | 外向き通信を許可するホスト |
network.deniedDomains | 9 個 | 許可リストにあっても拒否する(優先) |
外向き通信のスモークテスト (要 FIRECRAWL_API_KEY)
実際に sandbox 越しに通るかを確認:
firecrawl scrape "https://techblog.zozo.com/" -o /tmp/test.md
# → 成功すれば /tmp/test.md に markdown が書き出される
# → 失敗時 (HTTP/2 403 / x-deny-reason: host_not_allowed) は
# sandbox 設定が effective になっていない (CC 再起動を忘れた可能性)なぜ AI が自動で編集しないのか
~/.claude/settings.json は CC 自身を制約する security boundary。AI が自分の制約を勝手に緩める(self-tampering)のを防ぐため、CC の auto mode classifier と Edit(.claude/settings*) / Write(.claude/settings*) deny rule が 二重でブロックする。Bash 経由の迂回も classifier が「User Deny Rules circumvention」として deny する設計。
このため:
- AI 側: patch JSON を 提示するだけ
- ユーザー側: 手動で適用 + 検証
これは harness の 責任境界。AI に security 設定変更の自律権限は持たせない。
トラブルシューティング
編集後も 403 が出る
- JSON syntax error の可能性。
jq -e '.' ~/.claude/settings.jsonで確認 - CC を 完全再起動(cmd+Q → 再起動)。sandbox 設定は session start 時に読まれる
FIRECRAWL_API_KEY環境変数が未設定の可能性。.zshrcを確認
filesystem の write 許可を追加したのに EPERM が出る
⚠️ キー名は allowWrite(公式: code.claude.com/docs/en/sandboxing)。 write という名前にすると未知キーとして無視され、設定が効かない。 ~/ は sandbox 側で展開されるので tilde 形式でよい(公式例 ["~/.kube"])。
修正:
// ❌ 効かない(キー名が違う = 無視される)
"filesystem": { "write": ["~/.cursor"] }
// ✅ 効く(公式キー)
"filesystem": { "allowWrite": ["~/.cursor"] }ディレクトリ指定で配下も再帰的に許可される。
(cursor-agent backend の sandbox 要件として 2026-05-29 に実測確認。誤った write キーでの エラー例: EPERM: open '/Users/<user>/.cursor/cli-config.json.tmp')
別のドメインが必要になった
allowedDomains 配列に追加するだけ。CC 2.1.113+ では *.example.com の wildcard も使えるが、漏れの可視性のため明示列挙を推奨。
sandbox を一時的に外したい
"enabled": false にする。または --no-sandbox flag で起動。ただし security 後退するため一時利用に限る。
PROPOSAL (未適用・提案のみ): credential-masking + strictAllowlist による denyRead 代替 (133.6)
これは提案であり、live 設定への適用ではない。
~/.claude/settings.jsonは本タスクで 一切変更していない。適用判断はユーザー (オーナー) が行う。
docs/research/133-6-cc-cli-claim-verification.md で一次情報 (raw CHANGELOG.md) から確認した CC CLI の新機能 3 件は、.claude/rules/defense-layer-blast-radius.md が記録する 2 件の実事故 (gh CLI 設定ディレクトリを denyRead で塞いで credential helper が死亡 / sandbox.enabled で SSH alias 解決が破壊) を構造的に防げる可能性がある。
| CC バージョン | 機能 | 現行 denyRead との違い |
|---|---|---|
2.1.219 | sandbox.network.strictAllowlist | 未許可ホストを prompt 無しで deny。ファイル読み取りには関与しないため、gh/npm/ssh の設定ファイル読み取りは影響を受けない |
2.1.221 | credential file mode: "mask" (Linux/WSL 限定、macOS は deny フォールバック) | ディレクトリ丸ごと denyRead するのではなく、sandboxed プロセスにはダミー値を見せ、egress 時のみ sandbox proxy が実値へ差し替える。CLI 本体は自分の設定ファイルを正常に読めるまま、実際の secret だけが外部に出ない |
2.1.224 | extract / onExtractNoMatch / decode: "jwt" + maskClaims / awsPairs + sigv4 | 構造化 credential (JWT、AWS SigV4 署名) を値単位で masking。network.tlsTerminate が前提条件 |
なぜ既存事故を防げる可能性があるか: 2 件の事故はいずれも「設定とシークレットが同じ ディレクトリに混在しているファイルを、ディレクトリ単位で denyRead した」ことが原因 (~/.config/gh は gh CLI 本体が毎回読む設定と OAuth token が同居)。mode: "mask" は ディレクトリ単位ではなくファイル内の値単位で動作するため、CLI 本体からは「ファイルは読める、 値だけダミー」という状態になり、gh CLI が起動時に設定を読めず死亡する経路を構造的に回避できる 可能性がある。
未検証・要確認事項 (適用前に検証が必要、本タスクではここまで):
- macOS では credential file masking が
denyにフォールバックする (2.1.221 changelog 原文)。defense-layer-blast-radius.mdの 2 事故はいずれも macOS 実行環境のため、macOS では この提案の効果が限定的 (denyRead と同じ deny 挙動になる可能性が高い) — Linux/WSL 環境限定の 改善である可能性が高い network.tlsTerminate前提条件の運用コスト (TLS 中間者化が必要) は未調査strictAllowlistは network 側のみで、filesystem denyRead 問題そのものへの解決ではない (別軸の改善)- 実際の gh/npm/ssh 設定ファイルへの
mode: "mask"適用パターンの具体例は未検証
現行の推奨変更なし: 上記の未検証事項 (特に 1) により、現時点で denyRead からの置き換えを 推奨しない。macOS 中心の運用実態を踏まえ、Linux/WSL 環境での採用可否を先に検証してから .claude/rules/defense-layer-blast-radius.md の「追加前の必須チェック」に選択肢として 追記することを次の一手として提案する。
関連
templates/sandbox-settings.json.template— harness の reference 設定。本 recipe と 29 ドメイン allowlist + 9 ドメイン denylist が完全同期。新規プロジェクト (=sandbox既存無し = Case A) で一括流用するなら、template のsandboxセクション全体をコピーすると確実。既存 sandbox がある場合 (Case B) は jq merge を使うこと (template の丸ごとコピーは既存filesystem/failIfUnavailableを破壊する)CLAUDE.mdPermission Boundaries — sandbox 設定が AI による self-tampering 防止層と多層防御を構成.claude/rules/cross-repo-handoff.md— Layer 1 (server-side) / Layer 2/3 (client-side) の redact 設計- CC v2.1.108+ sandbox 仕様: 公式 docs の
sandboxセクション
履歴
- 2026-05-21: 初版作成。外部プロジェクトで Firecrawl が 403 になった事例を契機に docs 化