Claude Code の Docker Sandbox で API キーが渡らない原因
Claude Code を Docker Sandboxes(sbx)で動かしていて、2 つの問題に当たりました。ひとつは、サンドボックス内のエージェントに外部 API(今回は Gemini API)のキーを渡したいのに、sbx secret で入れたはずのキーが見えないこと。もうひとつは、gh CLI が大半のコマンドは動くのに一部だけ失敗することです。
どちらも「プロキシがネットワーク層で認証を注入する」というこの環境の設計を理解すると腑に落ちました。実測した挙動(2026-08 時点)と回避策を残します。環境の仕様は変わりやすいので、時点に注意してください。
(a) APIキーの注入 — sbx secret は万能ではない
まず期待した動作です。sbx secret set <sandbox> GEMINI_API_KEY -t <key> と入れれば、サンドボックス内の環境変数に GEMINI_API_KEY が現れる、と思っていました。
現れませんでした。走行中のサンドボックスに入れても出てこず、サンドボックスを再起動しても出てきません。シェルの環境変数だけでなく PID 1 の環境変数を直接確認しても不在でした。
tr '\0' '\n' < /proc/1/environ | sed 's/=.*/=<set>/'挙動から見えてきたのは、sbx secret の主用途が「任意の環境変数の注入」ではなく、GitHub などの特定サービス向けのプロキシ認証注入だということです。この環境では外向きの通信がホスト側のプロキシを通り、対応サービス宛のリクエストにはプロキシが Authorization ヘッダーを差し込みます。GitHub のトークンを sbx secret で入れると即座に効くのはこの仕組みで、環境変数として配られているわけではありません。実際、公式ドキュメント(Docker Docs の Credentials)にも、対応サービスは固定のセットで、それ以外の変数は別の手段で渡す旨が書かれています。
このプロキシが Gemini API のドメインにも認証を注入してくれるかというと、しません。サンドボックス内からキーなしで Gemini API を叩くと Google 本体から 403 が返ってきました。つまり経路は開いているが、認証は素通しです。
実用解: direct モードならホスト側に .env を置くだけ
私のサンドボックスはホストの作業ツリーがそのままマウントされる構成(direct モード)でした。この場合、いちばん単純な解はサンドボックス側の操作ではなく、ホスト側でプロジェクトに `.env` ファイルを置くことです。置いた瞬間にサンドボックスから見えます。
この方式の利点は、キーの受け渡し経路にチャットやコマンドラインが入らないことです。sbx exec で export GEMINI_API_KEY=... を書き込む方式(公式ドキュメントが案内している /etc/sandbox-persistent.sh への追記)でも動きますが、その場合はキーがコマンドラインに乗ります。エージェントとの会話ログやシェル履歴にキーを残したくなければ、ホスト側でファイルを置く方が筋がよいです。.gitignore に入っていればリポジトリにも上がりません。
確認コマンド集
切り分けに使ったコマンドです。キーの値を画面に出さずに存在だけ確認する形にしています。
# 環境変数にそれらしい名前があるか(値は出さない)
env | grep -iE "key|token" | sed 's/=.*/=<set>/'
# PID 1 の環境変数(「再起動すれば入るはず」の最終確認)
tr '\0' '\n' < /proc/1/environ | sed 's/=.*/=<set>/'
# 自分のサンドボックスが direct モードかどうか
[ -d /run/sandbox/source ] && echo "clone mode" || echo "direct mode"(b) gh CLI が半分だけ動く理由
同じプロキシ設計が、gh CLI の奇妙な挙動も説明します。
まず、gh はトークンなしでは起動を拒否します。しかしこの環境では実際の認証はプロキシが注入するので、ローカルにトークンは要りません。`GH_TOKEN=x` のようなダミー値を渡すと起動し、大半の API はプロキシ注入の認証で通ります。
「大半」であって全部ではありません。プロキシが注入するトークンの権限の外にあるものは失敗します。私が踏んだのは次の 2 つです。
gh pr checks— checks 系 API(annotations)が 403gh pr edit— GraphQL で deprecated なprojectCardsフィールドを参照して失敗
回避はどちらも REST 直叩きでした。
# CI の監視: gh pr checks の代わりにポーリング
gh run list --json databaseId,status,conclusion
# PR 本文の更新: gh pr edit の代わりに REST PATCH
gh api -X PATCH repos/<owner>/<repo>/pulls/<n> -F body=@file.mdいちばん危ない落とし穴は gh pr edit の失敗の仕方です。エラーを出しながら、本文は更新されていません。エラーメッセージが出た後の見た目では部分的に成功したのか失敗したのか判別できず、私は更新されたつもりで先に進みかけました。この環境で gh による更新系の操作をしたら、結果を必ず読み戻して検証する運用にしています。
gh pr view <n> --json body --jq .body | headこの構成で気をつけること
- `sbx secret` は「環境変数の注入」ではなく「対応サービスへのプロキシ認証注入」として理解する。対応サービス以外のキーは別経路で渡す
- 任意のキーは、direct モードならホスト側の
.env置きが最短で、ログにキーが残らない - 「入ったはず」の確認は
/proc/1/environまで見る。シェルのenvだけでは再起動待ちなのか不在なのか区別できない ghはダミートークンで起動し、プロキシ認証で動く。通らない API があることを前提に、更新系は読み戻しで検証する- この環境の仕様は動きます。ここに書いた挙動は 2026-08 時点の実測です
サンドボックス環境は「エージェントに秘密情報を直接持たせない」方向に設計されていて、それ自体は理にかなっています。エージェントがキーの値を読めなくても認証済みの通信はできる、というのがプロキシ注入方式の狙いです。今回の 2 つの問題はどちらも、その設計思想を知らずに「普通の Docker コンテナ」のつもりで操作したことから来ていました。