Claude Media
unshare(CLONE_NEWUSER)がClaude Codeのサンドボックスで失敗する原因

unshare(CLONE_NEWUSER)がClaude Codeのサンドボックスで失敗する原因

サンドボックス化したBashが「unshare(CLONE_NEWUSER): Invalid argument」で断続的に失敗する原因と、GitHub Issueで実測された回避策を解説します。

Claude Codeでsandbox.enabledを有効にすると、まったく同じBashコマンドを再実行すれば通るのに、最初の呼び出しだけ次のエラーで失敗することがあります。

apply-seccomp: unshare(CLONE_NEWUSER): Invalid argument

原因はコマンドの内容ではありません。GitHub Issue #86928は、Claude Code自身が内部で起動するサンドボックス用ヘルパーとLinuxカーネルの間で起きる、起動直後だけのタイミング競合だと突き止めました。

Claude Codeのサンドボックス化されたBashが、なぜランダムに失敗するのか

単一vCPUのGCPインスタンスで最初にこの不具合が報告されたとき、観測された失敗率はサンドボックス化されたBash呼び出しのおよそ1割でした。echo 1のような何もしないコマンドでも起き、失敗した直後に同じコマンドを再実行すると成功します。設定ミスでもネットワークの問題でもなく、Claude Codeのバイナリが起動するたびに毎回踏む可能性がある、起動処理そのものの不具合です。

Claude Codeがなぜサンドボックスという仕組みを持つのか、承認疲れを減らす二層分離の設計思想はClaude Codeの権限モデルの変遷で扱っており、本記事はこの特定の失敗の原因切り分けに絞ります。sandbox.enabled自体の設定挙動はsandbox.enabledはClaude CodeのBashコマンドを隔離する設定で確認できます。

unshare(CLONE_NEWUSER)がEINVALになる仕組み

この現象は典型的な競合状態(race condition)です。競合状態とは、複数の処理が完了するタイミングがわずかにずれることで、通常は起きないはずの中間状態が一瞬だけ発生し、結果に影響してしまう不具合を指します。

サンドボックスが有効なとき、Linux上のClaude CodeはARGV0=apply-seccompという名前で自分自身を再実行し、unshare(CLONE_NEWUSER)でユーザー名前空間(user namespace)を作ってからseccompフィルターを適用します。失敗する呼び出しのstraceは次の順序でシステムコールを記録しています。

clone(..., CLONE_THREAD|CLONE_CHILD_CLEARTID, ...) = 11810   # bun/mimallocがスレッドを起動
futex(child_tidptr, FUTEX_WAIT_BITSET, ...)                  # ヘルパーがスレッドをjoin
prctl(PR_SET_NAME, "mi-scavenger"); exit(0)                  # スレッドが終了
# futexが起床してjoinが返る
unshare(CLONE_NEWNS|CLONE_NEWPID)      = -1 EPERM            # 非特権なので想定どおり
unshare(CLONE_NEWUSER)                 = -1 EINVAL           # joinが返った約80マイクロ秒後

ヘルパーが使うbunランタイムはメモリーアロケーターにmimallocを採用しており、起動時にmi-scavengerという掃除用スレッドを立てます。ヘルパーはこのスレッドの終了をjoinで待ちますが、Linuxカーネル6.1系のkernel/fork.cでは、mm_release()がスレッドをスレッドグループから完全に外すrelease_task()より前に、joinしている側を起こしてしまいます。unshare(CLONE_NEWUSER)check_unshare_flags()でスレッドグループが空でないとEINVALを返す仕様なので、この一瞬に呼び出しが重なると失敗します。joinが「スレッドは終わった」と教えてくれても、カーネルの後始末はまだ終わっていません。

Issue報告者は、Claude Codeを介さない最小限のC言語コードでこの競合を再現しています。

最小再現コード(joinの直後にunshareを呼ぶだけのC言語プログラム)
#define _GNU_SOURCE
#include <sched.h>
#include <linux/futex.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
 
static int tid_slot;
static char tstack[65536];
static int thread_fn(void *arg) { return 0; }
 
int main(int argc, char **argv) {
  int iters = argc > 1 ? atoi(argv[1]) : 100;
  int sleep_us = argc > 2 ? atoi(argv[2]) : 0;
  int fails = 0, ok = 0, other = 0;
  for (int i = 0; i < iters; i++) {
    pid_t pid = fork();
    if (pid == 0) {
      tid_slot = -1;
      int tid = clone(thread_fn, tstack + sizeof(tstack),
                      CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|
                      CLONE_SYSVSEM|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID,
                      NULL, &tid_slot, NULL, &tid_slot);
      if (tid < 0) { perror("clone"); _exit(4); }
      int v;
      while ((v = __atomic_load_n(&tid_slot, __ATOMIC_SEQ_CST)) != 0)
        syscall(SYS_futex, &tid_slot, FUTEX_WAIT, v, NULL, NULL, 0);
      if (sleep_us) usleep(sleep_us);
      int r = unshare(CLONE_NEWUSER);
      _exit(r == 0 ? 0 : (errno == EINVAL ? 1 : 3));
    }
    int st; waitpid(pid, &st, 0);
    int code = WIFEXITED(st) ? WEXITSTATUS(st) : 9;
    if (code == 1) fails++; else if (code == 0) ok++; else other++;
  }
  printf("iters=%d ok=%d EINVAL=%d other=%d (post-join sleep %dus)\n",
         iters, ok, fails, other, sleep_us);
  return 0;
}
gcc -O2 race.c -o race
./race 200        # iters=200 ok=0   EINVAL=200 other=0 (post-join sleep 0us)
./race 200 50     # iters=200 ok=200 EINVAL=0   other=0 (post-join sleep 50us)

joinの直後に何もせずunshareを呼ぶと200回中200回が失敗し、50マイクロ秒待つだけで200回中0回に落ち着きます。競合の窓はマイクロ秒単位という狭さですが、確実に存在します。

どの環境でどれくらいの頻度で起きるか

観測頻度は環境ごとに大きく揺れ、しかも直感に反する傾向を示します。CPUに余裕があるアイドルなマシンほど失敗率が高く、負荷のかかったマルチコア機ほど低いという逆転が繰り返し報告されました。アイドル状態では、スレッドが終了した瞬間にjoin側が即座にスケジュールされ、カーネルの後始末が追いつかないためです。

報告環境コア数観測頻度備考
単一vCPUのGCP VM(aarch64、アイドル)コア数1観測頻度約10%備考最初の報告
ネストしたbwrapユーザー名前空間(x86_64、bare metal)コア数4観測頻度約13%(151回中19回)備考サブエージェント並列実行時
bare metalデスクトップコア数32観測頻度約33%(重い多エージェントセッション)備考同じ機材のアイドルループでは1/200
軽負荷のノートPC(x86_64)コア数14観測頻度約18%(45回中8回)備考「多コアなら起きない」を否定する事例
stockなx86_64 VM(4vCPU、アイドル)コア数4観測頻度600回中2回備考claudebotによる追試

コア数の多さだけでは説明がつかず、ネストの深さや瞬間的な負荷の有無のほうが結果を左右しています。さらに別の報告者による測定では、ページキャッシュがコールドかウォームかで発生率が10倍以上変わることも示されました。同一マシン・同一コマンドで、コールドキャッシュ直後は12.5%(200回中25回)、直後にウォームな状態で再測定すると1%(200回中2回)まで下がっています。実務上は、セッション開始直後の最初の数回のBash呼び出しでこのエラーに遭遇しやすく、その後は自然に減っていく傾向になります。

再試行だけでは済まない理由

このコマンド自体は、再実行すれば通ることが複数の報告者から確認されています。Issue報告者がこの不具合を掘り下げた理由は、失敗そのものではなく、失敗がClaudeの判断を誤らせた実例が2つ観測されたためです。

1つ目は誤診断です。あるセッションでは、Claudeがこのエラーに遭遇した際、「コマンドごとに新しいファイルシステム層が使われ、書き込みは毎回破棄される」という誤った仮説を立て、それをそのままユーザーに報告しました。実際にはただの起動時レースで、原因究明にセッションの時間を無駄に費やす結果になっています。

2つ目はより実害があります。あるセッションでは、Claudeが2回の一時的な失敗の後、3回目にdangerouslyDisableSandbox: trueを付けて同じコマンドを再試行し、成功しました。その結果、本来サンドボックスが読み取りをブロックしていたはずのファイルを読んでしまいます。Claudeはこの挙動を正直に報告したため報告者が気づけましたが、sandbox.allowUnsandboxedCommandsの既定値はtrueです。一時的なセットアップ失敗が、サンドボックス解除の引き金になり得ることを示す実例になっています。この既定値をfalseに変える運用はallowUnsandboxedCommandsは抜け道を管理設定で塞ぐ設定で扱っています。

Issue報告者は、ヘルパーの一時的な起動失敗と、サンドボックスが意図的にコマンドを拒否したポリシー拒否とを、Claudeに見える形で区別する余地があると指摘しています。現状はどちらも「サンドボックスに止められた」という同じメッセージで届きます。本来、サンドボックス外での再試行を検討してよい理由になるのは一時的な起動失敗のほうだけですが、Claudeからはその区別がつきません。

回避策と、それぞれの限界

Issueのコメント欄では複数の回避策が実測されていますが、効果と副作用の大きさは同じではありません。

対策効果(実測)トレードオフ
MIMALLOC_PURGE_DELAY=0を環境変数に設定効果(実測)3,800回中0回失敗(未設定は36回) / 14,000回中0回失敗(未設定は16回)トレードオフメインプロセスのメモリー解放が即時化しわずかに遅くなる。-1は解放自体を止めるため非推奨
PATH先頭にbwrapラッパーを置きunshare -U --map-current-user --keep-caps経由で起動効果(実測)サンドボックス化した呼び出しで15+17回中失敗0、bwrapループで0/100(対照は20/30が失敗)トレードオフPATHシムを自分で書いて保守する必要がある
外部サンドボックスラッパー(コミュニティ製)効果(実測)ヘルパーの自己再実行を経由しないため競合が原理的に起きないトレードオフサンドボックスの粒度がBash呼び出し単位からセッション単位に変わる
該当コマンドをサンドボックス対象外にする効果(実測)エラーそのものを回避トレードオフそのコマンドへの分離を丸ごと諦める

報告者自身が実運用したのは2番目の方式です。ARGV0=apply-seccomp /proc/self/fd/3という起動コマンドを、PATHの先頭に置いた小さなbwrapラッパーでunshare -U --map-current-user --keep-caps経由の起動に書き換えます。util-linuxのunshareはシングルスレッドで動くため、競合するシステムコール自体を踏みません。seccompフィルターは変わらず適用され、AF_UNIXソケットの拒否とuidの非マップも維持されることを確認済みです。

MIMALLOC_PURGE_DELAY=0は、mimallocが掃除用スレッドそのものを起動しなくなる設定で、.claude/settings.jsonenvブロックに書けます。

{ "env": { "MIMALLOC_PURGE_DELAY": "0" } }

MIMALLOC_PURGE_DELAY=0には検証の罠もあります。settings.jsonenvにキーを書いて、Bashツール呼び出しの中でenv | grep MIMALLOCが期待どおりの値を返しても、それはapply-seccompヘルパー自身に変数が渡っている証拠にはなりません。親プロセスが子プロセスに環境変数を渡していても、ヘルパーはサンドボックス内のPID名前空間の外側で動くため、/proc/1/environを直接確認する検証はサンドボックス自身に拒否されます。報告者が示した唯一の確認方法は、設定後に数百回のツール呼び出しを重ねてエラーが再発しないことを確かめることでした。

特定のコマンドだけをサンドボックスから外す運用はsandbox.excludedCommandsでdockerなど非対応コマンドを対象外にするで扱っている設定です。根本原因はClaude Code自身のヘルパーにあるため、他のコーディングエージェントがどう分離を実装しているかはAIコーディングエージェントのサンドボックス実装比較で比較しています。

v2.1.255以降でこの競合は再現しなくなったか

Issueは2026年8月15日に登録され、9月9日のコメント以降もopenの状態が続いています。ただし直近の実測は、再現しなくなったことを示す方向に振れています。

バージョン実行回数失敗回数
v2.1.236(apt配布)実行回数12,000回失敗回数18回(0.15%)
v2.1.255(Claude Desktop同梱)実行回数4,000回失敗回数0回
v2.1.260(Claude Desktop同梱)実行回数18,000回失敗回数0回

決め手は総数ではなく、同じループの中で1,000回ずつ交互に切り替えて実行した対照実験です。v2.1.236側だけが3回失敗し、v2.1.260側は0回でした。マシンやカーネルの条件は完全に同一なので、差はバイナリ側にあることになります。MIMALLOC_PURGE_DELAYを設定しない状態での測定である点も、環境変数の効果ではないことを裏付けています。

ただし測定者自身が「ここで再現しないことは、直ったことと同じではない」と釘を刺しています。この対照実験はビジーなマルチコア機によるもので、失敗率200/200を記録した単一vCPUのアイドル環境では追試されていません。さらにMIMALLOC_PURGE_DELAY=0によるカーネル時間の短縮効果は、v2.1.260でも変わらず観測されました。つまりmi-scavengerスレッド自体は今も起動しており、根本原因が消えたわけではなく、unshare呼び出しの周りに再試行や待機が入った可能性のほうが高いという見立てです。恒久修正として提案されているのもまさにこの方式です。

Issue本文は修正案の第一候補に、apply-seccomp内でunshareのEINVALを回数制限つきで再試行する方法を挙げており、その変更はanthropic-experimental/sandbox-runtimeの#479として提出されています。CHANGELOG.mdをv2.1.266まで確認しても該当する記述は見当たらず、別の報告者がClaude自身にCLIの差分を追わせても、サンドボックスラッパーや内部の再試行機構に変化は見つかりませんでした。挙動が変わったことは対照実験で示されていますが、Anthropic公式による修正の明記や、原因そのものが解消したという確認はまだありません。

よくある質問

macOSでも同じエラーは起きますか

起きません。この競合はLinux固有のunshare(CLONE_NEWUSER)とbubblewrapによるサンドボックス実装に起因します。macOSは組み込みのSeatbeltフレームワークでサンドボックスを構築しており、ユーザー名前空間もbubblewrapも経由しません。WSL2上のLinuxはbubblewrapを使うため、この不具合の対象に含まれます。

まとめ

unshare(CLONE_NEWUSER): Invalid argumentは、コマンドの内容や設定ミスではなく、Claude Codeのサンドボックスヘルパーが自分自身を再実行する際に、bunランタイムが起動するmi-scavengerスレッドの終了処理とカーネルの後始末が競合する、起動時だけの一過性のレースです。単一vCPUの環境やページキャッシュがコールドな状態、深くネストしたコンテナで発生率が上がり、同じコマンドを再実行すれば通ります。安全上の含意は、この失敗がdangerouslyDisableSandboxによるサンドボックス解除の引き金になり得る点です。sandbox.allowUnsandboxedCommandsfalseにしておけば、この経路自体を塞げます。恒久的な修正が広く確認されるまでの回避策としてはMIMALLOC_PURGE_DELAY=0が実測ベースで有効ですが、ヘルパー自身に届いているかを直接検証する手段はなく、数百回の呼び出しで再発しないことを確かめるのが実質的な確認方法になります。

この記事を共有:XはてブLinkedIn