はじめに
Re:Q Tech(TECH BLOG)をご覧いただきありがとうございます。
クラウド&ネットワーク技術統括部のN.Tです。
Claude Code に社内の作業フォルダを渡したら、認証情報や隣のプロジェクトのファイルまで読めてしまった、という経験はないでしょうか。
実はこれ、特別な事故ではなく、対策をしていないと普通に起きてしまいます。
エージェントは指示された通り、渡された範囲を忠実に読みに行くだけだからです。
今回は、Claude Code を Docker コンテナの中で動かし、社内に安全に配布するためのサンドボックスを作ってみます。
やりたいことは大きく 3 つです。
- ホスト側のファイルへの到達を、コマンドの種類によらず塞ぐ
- 持ち込んだファイルに機密情報が紛れていたら、起動前に検出する
- そのうえで利用者の手間はできるだけ増やさない
1. コンテナで隔離する
まず、個別のコマンドを制限する方式から試してみます。
Claude Code には permissions.blockReadsOutsideWorkingDirectories という設定があり、作業ディレクトリ外の読み取りを制限できます。
ただ、これは Read 系ツールにしか効きません。
試しに ~/.claude/settings.json を読ませてみたところ、Read ツールは拒否する一方で、Bash 経由の cat は普通に読めてしまいました。
head や sed、python に切り替えても同じで、コマンド文字列単位の deny ルールでは網羅しきれないようです。
これでは正直なところ塞ぎきれません。
そこで、コンテナという境界そのもので、ホスト側を見せないようにする方式に切り替えます。
マウントしていないパスには、コマンドの種類を問わず到達できないので、対策としてはシンプルです。
最低限の Dockerfile は以下のようになります。
FROM node:22-bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates git ripgrep \
&& rm -rf /var/lib/apt/lists/*
ARG CLAUDE_VERSION=latest
RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_VERSION}
# 非 root 実行。コンテナ内 root はマウントしたホスト側ファイルの所有者を壊すため避ける
RUN useradd --uid 1000 --gid 1000 -m -s /bin/bash dev
USER dev
WORKDIR /workspace
ENTRYPOINT ["claude"]
起動は次のコマンドで行います。
渡すディレクトリを明示的に絞るのが要点で、それ以外は一切マウントしません。
docker run -it --rm \
-v "${WORKDIR}:/workspace" \
--user 1000:1000 \
--security-opt no-new-privileges \
claude-sandbox:latest
--security-opt no-new-privileges で sudo 等による権限昇格を塞ぎ、--rm を付けているので /tmp やホームディレクトリへの書き込みはコンテナ終了時に消えます。
マウントした場所以外には何も残らない仕組みです。
2. 起動前に機密情報を検査する
マウント境界を引いただけでは、その内側に紛れ込んだ機密情報までは防げません。
作業フォルダに .env や秘密鍵が紛れていたら、そのままエージェントへ渡ってしまいます。
ここで気をつけたいのが、検査を本体コンテナの中でやらないことです。
本体コンテナ内で検査すると、検査する側とされる側が同じ箱に入ってしまい、エージェントが検査結果を読めるうえ検査自体を止めることもできてしまいます。
それでは関所として機能しません。
そこで検査は本体コンテナとは別の、検査専用のコンテナで行うことにします。
# 検査専用コンテナで作業フォルダを読み取り専用マウントし、
# ネットワークを遮断して検査する
docker run --rm \
-v "${WORKDIR}:/scan:ro" \
--network none \
claude-sandbox:latest \
gitleaks dir /scan --no-git --redact --exit-code 1
検出時(終了コード 1)や検査自体の失敗時(0 でも 1 でもない終了コード)は、本体コンテナを起動しません。
判定できないときは安全側に倒しておきます。
docker run --rm -v "${WORKDIR}:/scan:ro" --network none \
claude-sandbox:latest gitleaks dir /scan --no-git --redact --exit-code 1
code=$?
if [ "$code" -ne 0 ]; then
echo "機密情報の疑いがあるため起動を中止します(終了コード: $code)"
exit 1
fi
gitleaks の既定出力は検出件数しか出ず、どのファイルの何行目かが分かりません。
実運用では --report-format json で受けて、ファイル名・行番号・ルール ID だけを表示に使うのがおすすめです。
検出した実値そのものは画面に出さないようにしましょう。
--report-format json --report-path /tmp/result.json
誤検出は、作業フォルダ直下に置く .gitleaks.toml で個別に除外を宣言します。
ファイル名は .gitleaks.toml 固定で、gitleaks が自動検出するのはこの名前だけです。
任意の名前にすると読み込まれないので、ここは注意したいポイントです。
[extend]
useDefault = true
[[allowlists]]
description = "この作業フォルダで誤検出と判断したもの"
paths = ['''reference/sample-config\.yaml''']
regexes = ['''DUMMY-[A-Z0-9]{16}''']
[extend] useDefault = true を書き忘れると、既定の検出ルールごと失われてしまうので気をつけましょう。
allowlist はあくまで追加の許可であって、ルール自体を無効化する権限は利用者に渡さない設計にしています。
3. 認証をコンテナ内で完結させる
隔離と検査だけでは、配布物としてはまだ運用に乗りません。
ホスト側に Claude Code をインストールして認証させる運用にすると、配布先の台数分だけ認証情報の管理が発生してしまい、せっかく隔離した意味が薄れてしまいます。
そこで CLAUDE_CONFIG_DIR 環境変数を使い、コンテナ内の設定ディレクトリをホスト側のマウント先へ向けます。
これで、ホスト側に Claude Code を一切インストールせず、コンテナ内だけで認証を完結できます。
docker run -it --rm \
-e CLAUDE_CONFIG_DIR=/auth \
-v "${AUTH_DIR}:/auth" \
claude-sandbox:latest
初回起動時にコンテナ内で /login すれば、認証情報が /auth(ホスト側では ${AUTH_DIR})へ書き込まれ、永続化されます。
次回以降は認証済みの状態で起動できます。
CLAUDE_CONFIG_DIR=/auth
↓
/auth ←→ ${AUTH_DIR}(ホスト側、マウント)
auth フォルダはイメージには絶対に焼き込まないようにします。
.gitignore と .dockerignore の両方で除外しておきましょう。
焼き込んでしまうと、共有した相手に自分の認証情報がそのまま渡ってしまう事故になるので注意が必要です。
4. 標準設定を毎回配置し直す
認証情報を永続化用のマウントへ切り出すと、副作用が一つ出てきます。
イメージに焼いた標準設定が、そのマウントに覆われて消えてしまうのです。
配布物として複数人に使わせる以上、個人が設定を書き換えても次回起動時には標準へ戻したいところです。
そこでエントリポイントで毎回配置し直すことにします。
#!/bin/sh
# entrypoint.sh(要点のみ)
CONFIG_DIR="${CLAUDE_CONFIG_DIR:-/auth}"
rm -rf "$CONFIG_DIR/skills" "$CONFIG_DIR/rules"
cp -r /opt/claude-standard/skills "$CONFIG_DIR/skills"
cp -r /opt/claude-standard/rules "$CONFIG_DIR/rules"
cp /opt/claude-standard/settings.json "$CONFIG_DIR/settings.json"
exec claude "$@"
これだけだと、個人の拡張を置く場所がなくなってしまいます。
そこで個人のスキルやルールは、entrypoint が上書きするユーザースコープ側ではなく、作業ディレクトリの直下に置くことにします。
${WORKDIR}/ ← ホスト側でマウントする作業フォルダ(コンテナ内 /workspace)
└── .claude/
├── skills/<個人スキル名>/SKILL.md
└── rules/*.md
/workspace/.claude/ はプロジェクトスコープとして扱われ、entrypoint が配置し直すユーザースコープとは別物です。
Claude Code は設定をスコープごとに読み、上書きせず連結する仕様のため、標準の読み込み順(ユーザースコープ → プロジェクトスコープ)に乗せるだけで、標準設定と個人の拡張が両方読み込まれます。
追加のマウントや、両者を分離する処理は必要ありません。
5. 1 コンテナで複数セッションを扱う
最後に、日々の使い勝手にも触れておきます。
利用者はエディタとターミナルなど複数窓から同時に Claude Code を使いたくなるものです。
ところがコンテナを起動のたびに使い捨てにすると、同一コンテナ内でしか機能しない Claude Code のセッション間メッセージング機能が働きません。
そこでコンテナは常駐起動にし、実際の Claude Code プロセスは docker exec で都度起動する構成にします。
# 1本目: コンテナが無ければ作成して起動
docker run -d --name "${CONTAINER_NAME}" claude-sandbox:latest
docker exec -it "${CONTAINER_NAME}" claude
# 2本目以降: 同じコンテナへ相乗りする
docker exec -it "${CONTAINER_NAME}" claude
同じ作業フォルダには毎回同じコンテナへ相乗りさせたいので、コンテナ名は作業フォルダの絶対パスから機械的に算出します。
CONTAINER_NAME="claude-sandbox-$(echo -n "$WORKDIR" | sha256sum | cut -c1-12)"
さいごに
3 つの対策に共通しているのは、「境界をどこに引くか」という発想です。
ホスト側に何を見せるか、検査は誰がやるか、認証情報はどこに置くか。
使っているのは Docker の標準機能だけで、追加の管理基盤は必要ありません。
社内展開の設計で迷ったら、まずこの 3 点から見直してみてください。




