claude plugin evalがsops-nix環境でBash全滅する原因と回避策
sops-nixを使うNixOSでclaude plugin evalを回すと、Bashの呼び出しがすべてbwrapのmkdirエラーで失敗します。/run/secretsのシンボリックリンクが原因で、現状の逃げ道と切り分け方をまとめます。
sops-nixでシークレットを管理しているNixOSでclaude plugin evalに--allow-tools "Bash(git *)"を付けると、シェルを使うケースのBash呼び出しがすべて、コマンドが始まる前に失敗します。エラーはbwrap: Can't mkdir parents for /run/secrets.d/3: Permission deniedです。原因は、evalのサンドボックスが/run/secretsを拒否リストに入れ、シンボリックリンクの先まで解決してマスクしようとすることです。この問題はGitHub issue #105518で報告されています。
症状: Bashだけが全滅し、ReadとGrepは動く
報告の環境は、Claude Code 2.1.303(nixpkgsのビルド)、NixOS 26.11、bubblewrap 0.12.0です。bubblewrapとsocatはシステム全体に入っています。
再現の手順は短いものです。
- sops-nixを有効にし、シークレットを1つ以上定義する。これで
/run/secretsが/run/secrets.d/<世代番号>へのリンクとして作られる - 次のコマンドで、シェルコマンドを実行するケースを回す
claude plugin eval <プラグイン> --allow-tools "Bash(git *)"すると、そのケースのBash呼び出しがすべて冒頭のエラーで落ちます。エージェントは仕方なくReadとGrepに切り替えて作業を続けます。通常のセッションでサンドボックスを有効にした場合は、同じマシンでも問題なく動くと報告されています。
つまり、サンドボックスの導入自体やbubblewrapの欠落ではありません。evalの実行にだけ当たる拒否です。
なぜ/run/secretsで落ちるのか
プラグインevalの使い方にあるとおり、claude plugin evalはBashを許可すると、すべてのコマンドをOSレベルのサンドボックスの中で動かします。ホームディレクトリとClaude Codeの設定は読めなくなります。
issueによると、このサンドボックスには組み込みの読み取り拒否リストがあり、次のようなパスが並びます。
/var/run/secrets/run/secrets/run/credentials- ほかにもいくつか
各エントリは、bubblewrapの--tmpfsのようなマスクとして、実行前にマウントされます。ここで/run/secretsはシンボリックリンクをたどって実体のパスに解決されます。sops-nixのホストでは、解決先は/run/secrets.d/<世代番号>です。
壁になるのは権限です。sops-nixは/run/secrets.dと各世代のディレクトリをroot:keys、モード0751で作ります。一般ユーザーは通り抜けられますが、中身の一覧は取れません。bubblewrapは非特権で動くので、この実体パスの親ディレクトリを作る処理がPermission deniedで止まります。拒否エントリが1つマウントできないだけで、サンドボックス全体の起動が失敗します。
bwrap単体で再現できる
報告者はClaude Codeを介さず、bwrapだけで同じ挙動を再現しています。
bwrap --ro-bind / / --dev /dev --tmpfs /run/secrets.d/3 true
# bwrap: Can't mkdir parents for /run/secrets.d/3: Permission denied
bwrap --ro-bind / / --dev /dev --tmpfs /run/secrets.d true
echo $?
# 0世代ディレクトリを指すと失敗し、1つ上の/run/secrets.dを指すと成功します。親を作る権限がないのは世代ディレクトリの側だけで、/run/secrets.dそのものはマスクできるということです。この差が、報告者の挙げる修正案の根拠になっています。
手元の環境が同じ原因か切り分ける
エラー文の/run/secrets.d/<数字>と、リンクの実体を見比べるのが早道です。読み取りだけのコマンドで確かめられます。
ls -ld /run/secrets /run/secrets.d
stat -c '%U:%G %a %n' /run/secrets.d /run/secrets.d/*
ls /run/secrets.d見るべき点は3つです。
/run/secretsが/run/secrets.d/<番号>へのリンクになっているか/run/secrets.dと世代ディレクトリがroot:keysの751かls /run/secrets.dがPermission deniedになるか(一般ユーザーが一覧できない状態)
3つともそうなら、報告と同じ条件です。リンクが無い、あるいは/run/secrets.dが一覧できるなら、別の原因を疑う余地があります。
別の原因については、エラー文で見分けられます。
| エラー文の特徴 | 原因の系統 | 関連記事 |
|---|---|---|
Can't mkdir parents for /run/secrets.d/N | 原因の系統/run/secretsのリンク先をマスクできない | 関連記事この記事 |
Can't create file at .../.direnv/flake-inputs/... | 原因の系統拒否パスの途中にシンボリックリンクがある(nix-direnv) | 関連記事NixOSのbwrapエラー |
apply-seccomp: write /proc/self/setgroups ... Permission denied | 原因の系統ネストしたユーザー名前空間の制限(AppArmorなど) | 関連記事apply-seccompのsetgroups失敗 |
| 実行前に「sandbox cannot reliably exclude」系で拒否 | 原因の系統認証情報ストア内のリンク(Docker) | 関連記事Docker入りMacでの拒否 |
NixOSのパッケージ側の権限ビットが原因になる別系統もあります。こちらはNixOSのサンドボックス権限エラーにまとめています。
設定では逃げられない理由
通常のセッションなら、サンドボックスの設定でファイルシステム隔離を調整できます。しかしevalの実行は、あなたのユーザー設定を読みません。プラグインevalの仕様では、実行ごとに一時的なホーム、作業ディレクトリ、Claude Codeの設定が作られ、ユーザー設定、フック、CLAUDE.md、プロジェクト設定は読み込まれないと説明されています。管理者が配った管理設定だけが例外として効きます。
issue #105518は、この点を指して「設定に回避策がない」と書いています。eval実行にサンドボックス設定を渡せない問題は、別のissue #99269で報告されています。こちらはUbuntuでapply-seccompが失敗する事例で、allowAllUnixSocketsのようなユーザー設定の回避策が適用できないことが論点でした。原因は違っても、設定で逃げられない点は共通です。
もう1つ、sops-nix側にも逃げ道がありません。報告者によれば、sops-nixには/run/secretsのリンクの場所を動かすオプションがないとのことです。
現状で取れる選択肢
issueはopenで、changelogに対応する項目はありません。取れる手は限られます。
sops-nixホストでevalを回すときの選択肢
Bash不要のケースだけ回す
ReadとGrep、Write、Editだけで済むケースは、
--allow-toolsにBashを付けなければ、サンドボックス自体が使われません。Bashが前提のケースは別に分けます。evalの外でclaude -pを使う
#99269の報告者が採っている迂回策です。ケースのプロンプトを
claude -pで直接流し、グレーダーは自前で適用します。通常セッションの設定が効くので動きますが、evalランナーの隔離は失われます。コンテナの中で回す
sops-nixの
/run/secretsリンクが無い環境なら、この拒否は起きません。ただし、コンテナ内でbubblewrapを起動できる設定が別途必要です。
#99269のコメントには、DockerコンテナでBashサンドボックスを起動させるプロジェクトの紹介があります。bubblewrapを入れたコンテナをseccomp=unconfinedとsystempaths=unconfinedで起動する構成です。Ubuntu向けの話で、claude plugin eval自体を直すものではありません。隔離を弱める設定なので、信頼できるプラグインとケースに限ります。
結果の読み方に注意
サンドボックスが起動できない場合、評価結果はどうなるでしょうか。#99269では、シェルが必要なケースがwith/withoutの両方で0点になり、レポートにはサンドボックスが起動していない旨が出ないと報告されています。sops-nixの件ではエージェントがReadとGrepに切り替えるため、0点にならないケースもありえます。どちらにしても、Bash前提のケースのΔ(プラグインの寄与)は意味を持たなくなります。
見分け方は、--keep-tempで実行ごとのディレクトリを残し、トレースのBash呼び出しにbwrapのエラーが入っていないかを見ることです。スコアだけで判断せず、トレースを確認します。
claude plugin eval <プラグイン> --allow-tools "Bash(git *)" --keep-tempBash付きevalの前に走らせる事前チェック
CIで夜間にevalを回している場合、全ケースが終わってからBashの全滅に気づくと、モデル呼び出しの費用が無駄になります。bwrap単体の確認なら、モデルを呼ばずに数秒で済みます。次のスクリプトは、/run/secretsが実在するときだけ、実体パスのマスクを試します。
if [ -L /run/secrets ]; then
target=$(readlink -f /run/secrets)
bwrap --ro-bind / / --dev /dev --tmpfs "$target" true \
|| echo "eval: Bash付きケースは失敗する見込み: $target"
fiこれはissueの再現コマンドを一般化した確認で、Claude Codeが実際に作るマスクの全体を再現するものではありません。通ったからといってevalが動く保証はなく、失敗したときだけ今回の型だと言えます。
evalにはこのほか、Claude Code v2.1.269以上とgit 2.31以上という前提もあります。gitが古いと、ケースの実行前に止まります。症状が違うので混同しにくいのですが、git --versionが2.31未満ならgit 2.30で止まる原因も確認してください。
修正案はどうなっているか
報告者は直し方の候補を2つ挙げています。
- マウントできるいちばん近い親(ここでは
/run/secrets.d)をマスクする - ユーザーがもともと読めない対象なら、警告を出してそのエントリを飛ばす。sops-nixのシークレットは所有者がrootでモード0400なので、一般ユーザーは読めない
関連するissue #104685は、拒否パスの途中にシンボリックリンクがあるとBashが全滅する問題です。nix-direnvのGCルートが典型例で、そちらでも「マスクできないエントリは警告して飛ばす」案が出ています。報告者は、これが入れば今回の問題も直るとしています。
2つの問題は原因が違います。#104685は拒否パスの親がリンクで、bwrapが作成先を見つけられない型です。今回は実体パスの親に権限がなく、bwrapが親を作れない型です。ただ、どちらも「1つのエントリの失敗でサンドボックス全体が落ちる」という設計に行き着きます。修正が入ったかどうかは、更新前にchangelogで確かめられます。
まとめ
sops-nixのホストでBash付きのevalが落ちるのは、設定ミスでも依存の不足でもなく、拒否リストの/run/secretsがroot:keys・0751のディレクトリに解決されるためです。エラー文の/run/secrets.d/<番号>が見えたら、この型と考えて問題ありません。
回避の第一候補は、Bashを使うケースをevalの外に出してclaude -pで回すことです。評価の隔離は弱まりますが、結果が0点で埋まるよりは判断材料が残ります。修正を待つ場合は、issue #105518と#104685の進み方を見ておくと、変更の入った版がわかります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。