はじめに
Claude CodeやGitHub Copilot CLIをローカルで使い始めた時、ふと気になりました。
「このエージェント、.envの中身を読もうと思えば読めるよな」
Claude Codeであれば.claude/settings.jsonのdeny設定やignorePatterns、GitHub CopilotであればContent Exclusion設定(Business/Enterprise限定)やファイルタイプ単位での無効化といった方法で「読まないでください」とお願いすることはできます。しかしこれらはあくまでソフトウェア的な制約であり、エージェントが動いているディレクトリのファイルに原理的にアクセスできる状況は変わりません。
実際、Claude Codeの公式機能として.claudeignoreのようなファイルベースの除外機能はまだ存在せず(GitHubのIssueで要望中)、設定方法も非直感的です。特にエージェントモードでワークスペース全体を参照させる場合、設定ベースの制限が効かないケースも報告されています。
この記事では、「設定でお願いする」のではなく「原理的に読めない状態を作る」という方針でDockerを使って構成を組んだ話を紹介します。後から調べたら公式ドキュメントが概ね同じことを言っていたので、方向性としては妥当だったようです。
基本的な考え方
解決策はシンプルです。
読まれたくないファイルは、そもそもコンテナにマウントしなければいい。
コンテナに存在しないファイルは、どんなに優秀なエージェントでも読むことができません。設定ファイルによる「お願い」ではなく、物理的に存在しない状態を作ることが「原理的に安全」の意味です。
構成の全体像
今回想定している開発環境はVSCodeを使わず、エディタをdockerコンテナの中で動かすスタイルです。そのため、AIエージェント専用コンテナを追加する形で、compose.ymlにdevとclaudeの2サービスを共存させています。
ホストOS(最小限。ここでは作業しない)
├─ devコンテナ ← 開発者が作業する場所(Neovim, Git, AWS CLI等)
└─ claudeコンテナ ← AIエージェントが動く場所(プロジェクトのみ)
権限の分離設計
2つのコンテナに何をマウントするか・しないかが設計の核心です。
| マウント対象 | devコンテナ | claudeコンテナ |
|---|---|---|
プロジェクトファイル(/workspace) | ✅ | ✅ |
~/.ssh(SSH秘密鍵) | ✅ 読み取り専用 | ❌ |
~/.aws(AWSクレデンシャル) | ✅ 読み取り専用 | ❌ |
/var/run/docker.sock | ✅ | ❌ |
~/.gitconfig | ✅ 読み取り専用 | ❌ |
~/.claude(Claude認証情報) | ❌ | ✅(認証に必要) |
ClaudeコンテナにはSSH秘密鍵・AWSクレデンシャル・Docker socketが一切存在しません。エージェントがどれだけ積極的に動いたとしても、これらのファイルには原理的にアクセスできません。
/var/run/docker.sockのマウントはホストのroot権限と同義です(docker.sockにアクセスできればdocker run -v /:/host --privilegedでホストのファイルシステムをまるごとマウントできるため)。devコンテナにはあえてマウントしていますが、これはdockerコマンドを使う開発作業のためであり、claudeコンテナには意図的に渡していません。
具体的な実装箇所
.envなどの機密ファイルを隠す
プロジェクト全体(.:/workspace)をマウントする以上、その中に.envが含まれてしまいます。そこで/dev/nullを上書きマウントすることで、ファイルが「空として存在する」状態を作っています。
# compose.yml(claudeサービス)
volumes:
- .:/workspace
- /dev/null:/workspace/.env:ro
- /dev/null:/workspace/terraform/envs/dev/backend.conf:ro
- /dev/null:/workspace/terraform/envs/dev/terraform.tfvars:ro
- empty-terraform:/workspace/terraform/envs/dev/.terraform:ro
これは新しい技術ではなく、Dockerのバインドマウントの性質を利用した既知の手法です。ただし「全体をマウントしながら特定ファイルだけ除外する」方法としては有効です。
マルチステージビルドで最小構成を維持
Dockerfileはマルチステージビルドでdevとclaudeのイメージを分離しています。claudeステージにはNeovimやdotfilesは一切含まれず、Claude Codeの実行に必要なものだけが入っています。
FROM base AS claude
# Node.js + Claude Codeのみ追加
# ~/.sshや~/.awsのような認証情報はそもそも存在しない
#
# スクリプト内部でmanifest.jsonからSHA-256チェックサムを取得し、
# ダウンロードしたバイナリを検証してから実行する。
RUN curl -fsSL -o /tmp/claude-install.sh https://claude.ai/install.sh \
&& bash /tmp/claude-install.sh \
&& rm -f /tmp/claude-install.sh
後から調べてわかったこと
構成を組んだ後に公式ドキュメントや先行記事を調べたところ、いくつかのことがわかりました。
方向性は正しかった。 Anthropic公式のdevcontainerリファレンス実装も「プロジェクトディレクトリのみマウント、認証情報は渡さない」という同じ考え方を採用しています。Trail of Bits(著名セキュリティ企業)も同様のdevcontainerを公開しており、コンテナ隔離はセキュリティのベストプラクティスとして認識されています。
公式はネットワーク分離もしている。 Anthropic公式の実装はiptablesとipsetを使ってClaude CodeからのアウトバウンドをAnthropicのAPIなど必要最小限のドメインに制限しています。本構成ではここが未対応です。
GitHub Copilot CLIには公式サンドボックスがない(Linux)。 Claude Codeには公式のdevcontainerが用意されていますが、GitHub Copilot CLIにはLinux向けの公式サンドボックスが現時点で存在しません(macOS/WindowsはDocker Sandboxが対応)。本記事の構成は実績こそありませんが、原理的にはGitHub Copilot CLIにも同じアプローチが適用できると考えています。
残存リスク
ネットワーク分離がない。 これは意図的なトレードオフでもあります。
インタラクティブに使う場合——つまりエージェントがコマンドを実行するたびに人間が確認する運用では——ネットワークアクセスがあることで、その場でドキュメントを調べたり、パッケージをインストールしたりといった作業をエージェントに任せられます。ネットワーク分離するとこうした用途が制限されるため、日常的な開発補助として使う分には分離しない方が便利です。
リスクが顕在化するのは主に--dangerously-skip-permissionsで完全自律動作させる場合です。この状態でプロンプトインジェクション等によってエージェントが意図せず外部にデータを送出するリスクは、ネットワーク分離なしでは防げません。長時間無人で自律動作させる用途ではAnthropicのinit-firewall.shをentrypointに組み込むことを検討してください。
カーネルは共有される。 コンテナはホストのカーネルを共有するため、カーネルエクスプロイトによるコンテナ脱獄は原理的にゼロではありません。VMレベルの隔離が必要な場合はDocker Sandbox(microVM)を検討してください。
まとめ
「AIエージェントに読まれたくないファイルは、設定でお願いするのではなく、コンテナにマウントしなければいい」という原則から考えた結果、ファイルの扱いに関しては公式の推奨アプローチと概ね同じ結論に辿り着きました。
セキュリティにおける多層防御の考え方——設定による制限・マウントによる物理的除外——を組み合わせることで、実用的な安全性を確保できます。
ネットワーク分離はしなかったものの、「原理から考える」という方法は機能したと思っています。完璧な構成ではありませんが、AIエージェントをローカルで使い始める際の参考になれば幸いです。