Claude CodeでProcessPoolExecutorがmacOS sandboxで失敗する原因と回避
macOSのsandbox内でPythonのProcessPoolExecutorがPermissionErrorで止まる原因は、並列処理の本体ではなく起動前の上限チェックです。切り分け方と3つの回避策、それぞれの代償をまとめます。
macOSでClaude CodeのBashツールからPythonのProcessPoolExecutorを呼ぶと、PermissionError: [Errno 1] Operation not permittedで止まることがあります。並列処理そのものが禁止されているわけではありません。Pythonが起動前に行う「セマフォ数の上限チェック」が、sandboxに拒否されているのが原因です。
この記事では、症状の見分け方、拒否されているもの、設定でできる回避策とその代償を順に見ます。
症状:ProcessPoolExecutorの生成でPermissionError
報告されている再現コマンドは1行です。sandbox内のBashで実行します。
python3 -c "from concurrent.futures import ProcessPoolExecutor as P; \
print(list(P(2).map(abs, [-1, -2])))"sandboxの外なら[1, 2]が返ります。sandbox内では、次のようなトレースバックで止まります。
File ".../concurrent/futures/process.py", line 598, in _check_system_limits
nsems_max = os.sysconf("SC_SEM_NSEMS_MAX")
PermissionError: [Errno 1] Operation not permittedエラーが出る場所は、ワーカーの起動でもタスクの実行でもありません。ProcessPoolExecutorを作った時点です。報告された環境はClaude Code 2.1.311(2.1.309でも発生)、macOS 26、Apple Silicon、Python 3.13と3.14です。
原因:並列処理ではなく起動前の上限チェックが拒否される
CPythonのProcessPoolExecutor.__init__は、最初に_check_system_limits()を呼びます。この関数がos.sysconf("SC_SEM_NSEMS_MAX")でセマフォ数の上限を読みます。macOSでは、この値の取得がカーネルのsysctlの読み取りになります。
sandboxのSeatbeltプロファイルは、読めるsysctlを許可リストで絞っています。リストに該当のsysctlが入っていないため、読み取りが拒否され、sysconfがPermissionErrorを返します。報告者がカーネルログで確認した拒否は次の1行です。
Sandbox: Python(76588) deny(1) sysctl-read kern.sysv.semmnsCPythonのこの箇所が捕まえる例外はAttributeErrorとValueErrorだけです。PermissionErrorはOSErrorの仲間なので、そのまま外へ出ます。上限が読めなければ「並列は無理」と判断する作りではなく、例外で落ちる作りになっています。
並列に必要な部品は動く
同種の報告(#81032)は、起動前チェックを除いた部品を個別に試しています。
| 部品 | sandbox内の結果 |
|---|---|
名前付きPOSIXセマフォ(SemLock) | sandbox内の結果動く |
multiprocessing.Lock() | sandbox内の結果動く |
multiprocessing.Queue() | sandbox内の結果動く |
os.cpu_count() | sandbox内の結果動く |
os.sysconf("SC_SEM_NSEMS_MAX") | sandbox内の結果拒否される |
プロファイルにはもともと、Pythonのmultiprocessing向けにPOSIXセマフォを許可する行があります。許可の意図はあるのに、事前チェックの読み取りだけが漏れている形です。ただし、この表は報告者が自分の環境で試した結果です。ProcessPoolExecutor以外の並列処理(multiprocessing.Poolなど)が自分のコードで動くかは、実際に走らせて確かめてください。
2つの報告でsysctl名が違う
2件の報告は、拒否されているsysctlの名前が食い違います。
| issue | 起票 | 挙げているsysctl | 状態 |
|---|---|---|---|
| #81032 | 起票2026-07-25 | 挙げているsysctlkern.sysv.semmni | 状態2026-09-09にstaleでclose(not planned) |
| #105505 | 起票2026-10-27 | 挙げているsysctlkern.sysv.semmns | 状態起票されたまま |
#81032は、sandbox外でsysconfが返した87381がsysctl -n kern.sysv.semmniと一致したことを根拠にしています。#105505は、カーネルログの拒否行(kern.sysv.semmns)を根拠にしています。どちらが正しいかはissue上で決着していません。自分の環境で原因を確かめるときは、どちらかの名前を決め打ちせず、拒否ログを見るのが確実です。
自分の環境の問題かを切り分ける
次の2行で、症状が「この件」かどうかを判定できます。
python3 -c "import os; print(os.sysconf('SC_SEM_NSEMS_MAX'))"sandbox内でPermissionError、sandbox外(別のターミナル)で数値が返れば、この件です。両方とも数値が返るなら、原因は別にあります。ファイルの書き込み拒否やネットワーク拒否が疑わしいなら、拒否の出方が違うのでgit SSHだけ失敗する件のような個別の記事から当たってください。
エラーメッセージがSandbox is requiredのときは、まったく別の仕組みです。そちらはSandbox is requiredと拒否される原因にまとめています。
回避策は3つ、代償が違う
sandboxの設定で、sysctlの許可を足すことはできません。公式のsandboxingページにも、sysctlの許可を追加する設定の記載はありません。残る手は、コードを直すか、そのコマンドをsandboxの外で動かすかの2系統です。
3つの回避策の代償
単一プロセスにする
--workers 1のような切り替えをスクリプトに用意し、sandbox内ではそちらで動かします。sandboxは外れませんが、並列の速度は失います。
その呼び出しだけ外す
dangerouslyDisableSandboxによる再試行で、1回だけsandbox外で実行します。承認の経路は権限モードで決まります。
excludedCommandsに入れる
一致するコマンドは常にsandbox外で動きます。ファイルもネットワークも制限されなくなります。
単一プロセスにする
並列度を引数や環境変数で指定できるスクリプトなら、sandbox内の実行だけ1に固定します。たとえば、CLAUDE.mdに次のような一文を置いておくと、Claudeが最初から単一プロセスで走らせます。
macOSのsandbox内ではProcessPoolExecutorが生成時に失敗する。
sandbox内で試すときは --workers 1 を付ける。並列で回すのは手元のターミナルで行う。速度より「sandboxを外さない」ことを優先する場面向けの回避策です。
その呼び出しだけsandbox外で動かす
sandboxで失敗したコマンドは、ClaudeがdangerouslyDisableSandbox付きで再試行する場合があります。再試行は外で動くので、承認のされ方が権限モードで変わります。ManualモードとacceptEditsモードでは「Bash command (unsandboxed)」のプロンプトが出ます。bypassPermissionsでは確認なしで動きます。dontAskでは拒否されます。
allowUnsandboxedCommands: falseを設定している環境では、この再試行自体が無視されます。その場合は次のexcludedCommandsか、単一プロセス化しか選べません。
excludedCommandsに入れる
常にsandbox外で動かしたいなら、sandbox.excludedCommandsにパターンを書きます。
{
"sandbox": {
"enabled": true,
"excludedCommands": ["python3 scripts/convert_images.py *"]
}
}書くときに押さえる点が3つあります。公式の説明に沿うと、次のとおりです。
- 末尾を
*で終える。ワイルドカードのないパターンは完全一致で、引数付きの呼び出しに一致しません - 呼び出し全体を見る。
cd work && python3 scripts/convert_images.pyのように連結すると、すべてのコマンドが一致しない限りsandboxに残ります - ファイルへのリダイレクト(
> out.log)や$(...)を含む呼び出しは、sandboxに残ります
三つ目の点が地味に効きます。ログをファイルに落としながら並列スクリプトを走らせると、除外したつもりが効かず、同じPermissionErrorに戻ります。除外が効かない型はexcludedCommandsが効かない原因と既知バグで個別に扱っています。
代償:インタープリタの除外は広い
python3 *のようにインタープリタ全体を除外すると、Claudeが作ったスクリプトも外で動きます。公式は、インタープリタや作業ディレクトリ内のスクリプトを覆うパターンを書くと、Claudeがそのファイルを書き、sandbox外で実行できると警告しています。特定のスクリプトに絞ったパターンでも、そのスクリプトをClaudeが書き換えられるなら、同じ穴が残ります。
並列処理を戻すために、ファイルとネットワークの制限まで手放すことになります。#81032の報告者が、この選択肢はsandbox全体を必要以上に無効にするよう利用者を誘導すると書いた理由はここです。除外の書き方の基本はsandbox.excludedCommandsでdockerなど非対応コマンドを対象外にするにあります。
直るのを待つか、回避するか
修正案は#105505も#81032も同じで、macOSのsysctl-read許可リストに1行足すだけです。報告者は#105505で、バイナリ内のプロファイルを書き換えて試し、ProcessPoolExecutorが動いたと書いています。ほかの規則は不要だったとのことです。読み取り専用のシステム上限なので、露出するものはほとんどない、というのが報告者の見立てです。
一方、同種の#81032はstaleでcloseされており、起票から修正まで時間がかかる可能性があります。待つ場合は、バージョンを上げるたびに冒頭の1行を再実行して確かめます。直っていれば[1, 2]が返ります。
よくある混同
- dockerのexcludedCommandsと同じ話か:設定の仕組みは同じですが、原因は別です。dockerは公式が「sandboxと非互換」と明記するコマンドで、今回はPythonの事前チェックの拒否です
- Linuxでも起きるか:報告は
platform:macosのラベルで、sysctlの拒否はSeatbelt固有の話です。Linuxの隔離はbubblewrapで、Seatbeltとは仕組みが違います - macOS固有の制限はほかにもあるか:Apple Eventsを既定で遮断する件が代表です。
openやosascriptが-600で失敗する場合はallowAppleEventsの記事を見てください
まとめ
ProcessPoolExecutorの失敗は、sandboxが並列を禁じているのではなく、起動前の上限チェックがsysctlの許可リストに阻まれる現象です。設定でその1行を足すことはできません。まず単一プロセスで逃げ、どうしても並列が要るなら、その呼び出しだけを承認つきで外に出すのが、失うものの少ない順です。excludedCommandsは最後の手段で、書くなら対象スクリプトを最小にし、リダイレクトを含めない呼び出し方に揃えます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。