前置き
この記事は私がClaude Codeで個人開発をするにあたりChatGPTと壁打ちした記録を基にAIライティングしたものです。あくまで私が行き着いた方法に過ぎないことをご容赦ください。
はじめに
Dev Container内でClaude Codeを使っていたところ、以前は問題なく通っていたgitコマンドが突然失敗するようになりました。
通常のターミナルから実行するssh -Tやgit statusは成功します。しかし、Claude Code経由で実行した場合だけ失敗します。
最初に出たエラーは次のものでした。
bwrap: No permissions to create new namespace, likely because the kernel does not allow non-privileged user namespaces.
その後、seccomp、/procマウント、~/.gitconfigの読み取り拒否、さらにsandbox内だけに見える謎の未追跡ファイルまで、複数の問題が順番に表面化しました。
最終的には、Dev ContainerとClaude Code Sandboxを重ねる場合、gitやsshはsandbox外へ逃がすのが現実的だという結論になりました。
この記事では、その調査過程と対処をまとめます。
前提環境
今回の環境は次のような構成です。
Claude Code: 2.1.144
OS: Debian GNU/Linux 12 bookworm
実行環境: VS Code Dev Containers
コンテナ内ユーザー: node
Claude Code sandbox: enabled
Claude CodeのLinux/WSL2向けsandboxでは、bubblewrapとsocatが使われます。
そのため、bwrapという単語が出ること自体はCodex CLI固有の現象ではありません。Claude Codeのsandboxでも自然に発生します。
結論
今回の結論は次の通りです。
Dev Container内でClaude Code Sandboxを有効化する場合、
git / ssh は sandbox 外へ逃がすのが安定しやすいです。
推奨するClaude Code設定例です。
{
"sandbox": {
"enabled": true,
"enableWeakerNestedSandbox": true,
"autoAllowBashIfSandboxed": true,
"allowUnsandboxedCommands": false,
"excludedCommands": [
"git *",
"ssh *"
],
"network": {
"allowedDomains": [
"registry.npmjs.org",
"registry.npmjs.com",
"npmjs.com",
"npmjs.org",
"github.com",
"api.github.com",
"uploads.github.com",
"*.githubusercontent.com",
"cdn.playwright.dev",
"playwright.download.prss.microsoft.com"
]
}
}
}
gitやsshは、~/.gitconfig、~/.ssh/config、SSH AgentのUnix socket、GitHubへのネットワーク接続、.gitディレクトリなど、多くの外部要素に触れます。
これらをClaude Codeのsandbox内に閉じ込めると、想定外のファイルシステム表示や権限制御に巻き込まれやすくなります。
二重sandboxという構造
今回の難しさは、Dev ContainerとClaude Code Sandboxの二重構造にあります。
VS Code Dev Containers
↓
Docker / container runtime
↓
Debian container
↓
Claude Code
↓
bubblewrap / socat sandbox
↓
git / ssh / npm / build tools
この構造では、単なるgit statusでも次の要素が絡みます。
- Dockerのseccomp
- AppArmorやmount namespace
- bubblewrapのnamespace作成
- /procマウント
- Claude Codeのfilesystem制限
- Claude Codeのnetwork allowlist
- ~/.gitconfigの読み取り
- SSH AgentのUnix socket
- Gitのglobal configやignore設定
そのため、「Gitが失敗した」と見えても、Git自身ではなく、その前段のsandbox構築で失敗していることがあります。
最初の症状
Claude Code経由でgitやsshを実行すると、次のエラーが出ました。
bwrap: No permissions to create new namespace, likely because the kernel does not allow non-privileged user namespaces.
これは、GitやSSHに到達する前に、Claude CodeがBashコマンドを実行するためのsandboxを作成できていない状態です。
構造としては次のような失敗です。
Claude Code
↓
bubblewrap / bwrap sandbox
↓
namespace作成に失敗
↓
git / sshまで到達しない
seccompの確認
まず、コンテナ内でseccompの状態を確認しました。
grep Seccomp /proc/self/status
結果は次のような状態でした。
Seccomp: 2
Seccomp_filters: 1
Seccomp: 2は、seccomp filterが有効であることを示します。
Dockerのデフォルトseccompプロファイルは、cloneやunshareなどのnamespace関連操作に影響します。そのため、bubblewrapがnamespaceを作成しようとして失敗する場合、seccomp=unconfinedが対処候補になります。
Dev Container側の設定
Dev Containerでseccompを緩める場合は、.devcontainer/devcontainer.jsonに次のように設定します。
{
"runArgs": [
"--security-opt",
"seccomp=unconfined"
]
}
または、Dev Container仕様に寄せるなら次の書き方もあります。
{
"securityOpt": [
"seccomp=unconfined"
]
}
Docker Composeを使っている場合は、Compose側に書く必要があることもあります。
services:
app:
security_opt:
- seccomp=unconfined
設定後は、コンテナの再作成が必要です。
Docker起動オプションの反映確認
seccomp=unconfinedを設定した場合は、実際にDockerの起動オプションとして反映されているか確認します。
ホスト側で次のコマンドを実行します。
docker inspect <container_id_or_name> \
--format 'SecurityOpt={{json .HostConfig.SecurityOpt}} Privileged={{.HostConfig.Privileged}} CapAdd={{json .HostConfig.CapAdd}} Runtime={{.HostConfig.Runtime}}'
期待値は次のような形です。
SecurityOpt=["seccomp=unconfined"] Privileged=false CapAdd=null Runtime=runc
コンテナ内では再度seccompの状態を確認します。
grep Seccomp /proc/self/status
seccomp=unconfinedが反映されていれば、次のようになります。
Seccomp: 0
Seccomp_filters: 0
/procマウントの失敗
seccompの問題を解消すると、次は別のエラーが出ました。
bwrap: Can't mount proc on /newroot/proc: Operation not permitted
これは、bubblewrapがnamespace作成までは進んだものの、sandbox内で/procをマウントしようとして失敗している状態です。
Dev Container内でさらにClaude Code Sandboxを動かすため、ネストしたsandboxの問題になっています。
enableWeakerNestedSandboxによる回避
Claude Codeには、Dockerのようなネスト環境向けにenableWeakerNestedSandboxという設定があります。
Claude Code側の設定に次を追加しました。
{
"sandbox": {
"enabled": true,
"enableWeakerNestedSandbox": true
}
}
この設定により、/procマウントの問題は前進しました。
ただし、名前の通り通常のsandboxより弱いモードです。Dev Containerという外側の隔離があることを前提に、必要性を理解した上で使うべき設定です。
.gitconfigの読み取り拒否
seccomp=unconfinedとenableWeakerNestedSandboxを設定したところ、次はGitの設定ファイル読み取りで失敗しました。
warning: unable to access '/home/node/.gitconfig': Permission denied
warning: unable to access '/home/node/.gitconfig': Permission denied
warning: unable to access '/home/node/.gitconfig': Permission denied
fatal: unknown error occurred while reading the configuration files
ここまで来ると、bwrap自体はかなり動いています。
問題は、Gitが通常の動作として~/.gitconfigを読みにいったところ、Claude Codeの権限設定で拒否されていたことでした。
Claude Code側に次のような設定があると、Git自身の動作にも影響します。
{
"permissions": {
"deny": [
"Read(~/.gitconfig)"
]
}
}
これは、Claude Codeに個人のGit設定を不用意に読ませないための防御としては自然です。
しかし、Claude CodeのsandboxがBashコマンドとその子プロセスにも効くようになると、git自身が必要とする~/.gitconfigの読み取りまで拒否されます。
その結果、Gitコマンド全体が失敗します。
sandbox内だけに見える未追跡ファイル
さらに検証を進めると、別の奇妙な現象が出ました。
Claude Code sandbox内でgit statusを実行すると、通常ターミナルでは表示されない未追跡ファイルが表示されました。
Untracked files:
.bash_profile
.bashrc
.claude/commands
.gitconfig
.gitmodules
.idea
.mcp.json
.profile
.ripgreprc
.vscode
.zprofile
.zshrc
通常ターミナルでは表示されません。
最初は、カレントディレクトリやGit worktreeの違いを疑いました。しかし、.gitは通常のディレクトリでした。
drwxr-xr-x 16 node node 512 May 22 19:07 .git
そこで、Claude Code sandbox内でこれらのファイルの種類を確認しました。
for f in .bash_profile .bashrc .gitconfig .gitmodules .mcp.json .profile .ripgreprc .zprofile .zshrc; do
[ -e "$f" ] && stat -c '%n %s bytes %A %U:%G %F' "$f"
done
結果は次のようなものでした。
.bash_profile 0 bytes crw-rw-rw- nobody:nogroup character special file
.bashrc 0 bytes crw-rw-rw- nobody:nogroup character special file
.gitconfig 0 bytes crw-rw-rw- nobody:nogroup character special file
.gitmodules 0 bytes crw-rw-rw- nobody:nogroup character special file
.mcp.json 0 bytes crw-rw-rw- nobody:nogroup character special file
.profile 0 bytes crw-rw-rw- nobody:nogroup character special file
.ripgreprc 0 bytes crw-rw-rw- nobody:nogroup character special file
.zprofile 0 bytes crw-rw-rw- nobody:nogroup character special file
.zshrc 0 bytes crw-rw-rw- nobody:nogroup character special file
これは通常ファイルではありません。
character special fileで、所有者がnobody:nogroup、権限がcrw-rw-rw-です。見え方としては、/dev/nullのようなデバイスノードに近いものです。
つまり、Claude Code sandboxが保護対象のdotfileを隠すために、/dev/null相当のplaceholderを見せており、それが作業ディレクトリ側にも現れている可能性があります。
そのplaceholderを、sandbox内で実行されたgit statusが未追跡ファイルとして拾っていました。
phantom dotfilesという副作用
この現象は、通常のリポジトリ汚染ではありません。
実ファイルが作られているというより、Claude Code sandbox内のmount namespaceでだけ見えるphantom dotfiles、またはstub deviceのようなものです。
構造としては次のように見えます。
Claude Code sandbox起動
↓
保護対象dotfileを通常ファイルとして読ませないためにplaceholderを用意
↓
sandbox内では .bashrc や .gitconfig が character special file として見える
↓
git status がそれをUntracked filesとして拾う
↓
通常ターミナルでは見えない
この状態でgit add .のような操作をClaude Code sandbox内から実行するのは避けるべきです。
Gitだけでなく、ビルドツールやパッケージングツールがこれらのspecial fileを拾うと、別のエラーにつながる可能性があります。
.gitignoreで隠すべきではない理由
一見すると、次のように.gitignoreへ追加すればよさそうに見えます。
.bash_profile
.bashrc
.gitconfig
.gitmodules
.mcp.json
.profile
.ripgreprc
.zprofile
.zshrc
しかし、これはあまりおすすめしません。
理由は、将来本当に誤って.gitconfigや.mcp.jsonがリポジトリに作られたときに見落とす可能性があるためです。
今回の問題は、Gitのignore設定で隠すより、Gitコマンド自体をClaude Code sandbox外で実行する方が安全です。
GitとSSHをsandbox外へ逃がす設定
最終的には、gitとsshをsandbox対象外にすることで安定しました。
{
"sandbox": {
"enabled": true,
"enableWeakerNestedSandbox": true,
"autoAllowBashIfSandboxed": true,
"allowUnsandboxedCommands": false,
"excludedCommands": [
"git *",
"ssh *"
],
"network": {
"allowedDomains": [
"registry.npmjs.org",
"registry.npmjs.com",
"npmjs.com",
"npmjs.org",
"github.com",
"api.github.com",
"uploads.github.com",
"*.githubusercontent.com",
"cdn.playwright.dev",
"playwright.download.prss.microsoft.com"
]
}
}
}
allowUnsandboxedCommandsをfalseにしたままでも、excludedCommandsで明示したコマンドはsandbox対象外にできます。
GitやSSHはリポジトリ状態や認証情報に関わるため、sandbox内で無理に実行するより、明示的に外へ逃がす方が現実的です。
Claude Code Sandboxの現状
今回の一連の挙動を見ると、Claude Code Sandboxは実用に乗っている一方で、Dev Container内でさらに使うにはまだ境界条件が多いと感じます。
特に、次のような構成では躓きやすいです。
Dev Container
+ Docker seccomp
+ Claude Code bubblewrap sandbox
+ Git
+ SSH Agent
+ dotfile保護
bubblewrap自体が低レイヤーのnamespaceやmountを扱う仕組みなので、Dockerコンテナ内でさらに動かすと難易度が上がります。
そのため、評価としては次のような感覚です。
ローカル直実行:
比較的素直に使えそうです。
Dev Container内:
seccompや/procマウントで躓く可能性があります。
Git/SSH/Agent絡み:
sandbox外へ逃がす方が無難です。
Claude Code Sandboxそのものが使えないというより、Dev Containerとの二重sandboxでは、GitやSSHのような外部依存の強いコマンドをすべて閉じ込めるのはまだ難しい、という理解が近いです。
いつから表面化したのか
今回の現象は、Claude Codeの更新後に表面化しました。
Claude Code 2.1.133では、Linux/WSL向けにsandbox.bwrapPathとsandbox.socatPathのmanaged settingsが追加されています。
これは、bubblewrapやsocatの検出・指定まわりに関係する変更です。
以前はsandbox.enabledがtrueでも、bubblewrapやsocat経路が成立せず、実質的に弱い状態やsandboxなしでフォールバックしていた可能性があります。
その後、Claude Codeの更新やDev Container再作成により、bubblewrapとsocatが検出され、sandboxが実効的に動くようになった可能性が高いです。
つまり、今回の表面化は次の流れと考えると自然です。
以前:
sandbox.enabled は true
しかし bwrap / socat 経路が十分に成立していなかった可能性
更新後:
bwrap / socat sandboxが実効化
Dockerのseccomp制限が表面化
/procマウント制限が表面化
~/.gitconfigのRead拒否がGitに影響
phantom dotfilesがgit statusに表示
確認コマンド集
今回の切り分けで使った確認コマンドです。
seccompの状態確認です。
grep Seccomp /proc/self/status
Docker起動オプションの確認です。
docker inspect <container_id_or_name> \
--format 'SecurityOpt={{json .HostConfig.SecurityOpt}} Privileged={{.HostConfig.Privileged}} CapAdd={{json .HostConfig.CapAdd}} Runtime={{.HostConfig.Runtime}}'
socatの有無の確認です。
command -v socat
socat -V
Debianパッケージとしての状態確認です。
dpkg-query -W -f='${Package} ${Version} ${Status}\n' socat
apt-cache policy socat
socatがいつ入ったかを見るには、aptやdpkgのログを確認します。
zgrep -hE ' install socat| upgrade socat' \
/var/log/dpkg.log* \
/var/log/apt/history.log* 2>/dev/null
Git設定ファイルの権限確認です。
ls -la /home/node/.gitconfig
stat -c '%U:%G %a %n' /home/node/.gitconfig
namei -l /home/node/.gitconfig
git config --global --list --show-origin
sandbox内だけに見えるdotfileの種類確認です。
for f in .bash_profile .bashrc .gitconfig .gitmodules .mcp.json .profile .ripgreprc .zprofile .zshrc; do
[ -e "$f" ] && stat -c '%n %s bytes %A %U:%G %F' "$f"
done
Gitの実行場所確認です。
pwd
git rev-parse --show-toplevel
git rev-parse --git-dir
test -f .git && cat .git || ls -ld .git
セキュリティ上の注意
seccomp=unconfinedは、Dockerのデフォルトseccompプロファイルを外す設定です。
これはbubblewrapを動かすために有効なことがありますが、コンテナ全体の制限を弱めます。
また、enableWeakerNestedSandboxも名前の通り、通常より弱いsandboxです。
そのため、次のような方針が良いです。
- まず原因を切り分ける
- 何を緩めているのかを理解する
- Git/SSHはexcludedCommandsで逃がす
- 危険なファイル操作や生成コード実行はsandbox内に残す
- Read拒否設定が子プロセスに影響しないか確認する
~/.gitconfigには、ユーザー名、メールアドレス、署名設定、include設定、credential helper設定などが含まれることがあります。
Claude Codeに読ませたくない意図は妥当です。
ただし、Git自身も読むファイルであるため、単純にRead(~/.gitconfig)を拒否すると、Gitコマンドが壊れる可能性があります。
得られた教訓
今回の調査で得られた教訓は次の通りです。
- bwrapエラーはCodex CLI固有ではなく、Claude Code Sandboxでも発生する
- Dev Container内でClaude Code Sandboxを使うと、Dockerのseccomp制限に当たることがある
- enableWeakerNestedSandboxはネストしたコンテナ環境で有効な対処になりうる
- サンドボックスが実効化すると、Gitなどの子プロセスにもファイルアクセス制限が効く
- Read(~/.gitconfig)の拒否は、Git自身の設定読み取りを壊す可能性がある
- Claude Code Sandbox内では、dotfileがcharacter special fileとして見えることがある
- sandbox内のgit statusは、通常ターミナルと異なる結果になることがある
- Dev Container内では、git / sshをsandbox外へ逃がす運用が安定しやすい
まとめ
今回の問題は、単純なGit設定の不具合ではありませんでした。
表面的には「Claude Code経由でGitが通らない」という問題でしたが、実際には次の複数の層が絡んでいました。
- Dockerのseccomp
- Claude Codeのbubblewrap sandbox
- ネストした/procマウント
- ~/.gitconfigへのRead拒否
- sandbox内だけに見えるphantom dotfiles
特に印象的だったのは、Claude Code sandbox内のgit statusだけで、.bashrcや.gitconfigが未追跡ファイルとして表示されたことです。
確認すると、それらは通常ファイルではなく、nobody:nogroup所有のcharacter special fileでした。
この結果から、Dev ContainerとClaude Code Sandboxを重ねる場合、GitやSSHのような外部依存の強いコマンドはsandbox外へ逃がす方が安全であると判断しました。
最終的な運用方針は次の通りです。
Claude Code Sandboxは有効にする
ただし git / ssh は excludedCommands でsandbox外へ逃がす
危険なファイル操作や生成コード実行はsandbox内に残す
Dev ContainerとClaude Code Sandboxの二重sandboxは、防御層としては魅力的です。
しかし現時点では、Git、SSH Agent、dotfile、mount namespaceが絡むとかなり躓きやすいです。
「sandboxを有効化すれば安全で安定する」と単純に考えるのではなく、壊れやすいコマンドを見極めて、必要なものだけsandbox外に逃がす設計が重要だと感じました。
参考資料
- Claude Code Docs - Sandboxing
https://code.claude.com/docs/en/sandboxing - Claude Code Docs - サンドボックス
https://code.claude.com/docs/ja/sandboxing - Claude Code Changelog
https://code.claude.com/docs/en/changelog - Dev Container metadata reference
https://devcontainers.github.io/implementors/json_reference/ - Docker Docs - Seccomp security profiles for Docker
https://docs.docker.com/engine/security/seccomp/ - VS Code Docs - Sharing Git credentials with your container
https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials