sandbox-exec und die macOS-Sandbox erklärt

Jede App aus dem Mac App Store läuft in einer Sandbox. Denselben Mechanismus kannst du von der Kommandozeile aus für jedes beliebige Programm haben, und fast niemand tut es - weil das Werkzeug als veraltet markiert ist, die Profilsprache nicht dokumentiert wird und der erste Versuch an einer Fehlermeldung scheitert, in der die Sandbox nicht vorkommt.

Diese Seite ist das, was wir gern gehabt hätten, bevor wir BX darauf gebaut haben.

Was es ist und was nicht

Die Sandbox von macOS heißt Seatbelt. Sie wird im Kernel durchgesetzt, der betroffene Prozess kann sie nicht abschalten, und sie wird vererbt: alles, was der eingeschränkte Prozess startet, ist genauso eingeschränkt. Diese letzte Eigenschaft ist der ganze Punkt. Eine Regel, die nur für das Programm gilt, das du gestartet hast, aber nicht für den Paketmanager, den es aufruft, wäre wenig wert.

Das lohnt sich abzugrenzen von dem, was die meisten meinen, wenn sie sagen “macOS hat nach Erlaubnis gefragt”:

SeatbeltTCC
Der Dialog, den du kennstkeiner“möchte auf deinen Ordner Dokumente zugreifen”
Festgelegt durchein Profil, vor dem Startdich, beim ersten Zugriff
UmfasstDateisystem, Netz, IPC, Syscallseine Handvoll geschützter Ordner und Geräte
Kindprozesseerbenerben von der verantwortlichen App
Voreinstellungwas im Profil stehterlaubt, bis gefragt wird

TCC bewacht Dokumente, Schreibtisch, Downloads, Fotos, die Kamera. Es bewacht nicht ~/.ssh, nicht ~/.aws und keinen anderen Punkt-Ordner - also genau die Stellen, an denen das liegt, was für Entwickelnde zählt. Seatbelt tut es, wenn du es ihm sagst.

Das kleinste funktionierende Beispiel

sandbox-exec -f nur-tmp.sb /bin/zsh
(version 1)
(deny default)

(allow process-exec process-fork)
(allow sysctl-read mach-lookup signal)

(allow file-read*
  (subpath "/usr") (subpath "/bin") (subpath "/sbin")
  (subpath "/System") (subpath "/Library")
  (literal "/dev/null") (literal "/dev/urandom") (literal "/dev/tty"))

(allow file-read* file-write*
  (subpath "/private/tmp"))

Du bekommst eine Shell, die das System lesen und nach /tmp schreiben darf. cat ~/.ssh/id_ed25519 antwortet mit “Operation not permitted”, und alles, was diese Shell startet, ebenfalls.

-p nimmt das Profil stattdessen direkt entgegen, praktisch für einen Einzeiler und unangenehm für alles Längere:

sandbox-exec -p '(version 1)(deny default)(allow file-read*)' /bin/ls ~

Die Profilsprache

Profile sind S-Ausdrücke, ausgewertet von einem kleinen Scheme in der Sandbox-Bibliothek. Eine Regel besteht aus einer Operation und keinem bis mehreren Filtern, und die letzte passende Regel gewinnt - deshalb steht (deny default) oben, und alles danach öffnet wieder.

Operationen, die du wirklich brauchst:

file-read*            Daten und Metadaten lesen
file-read-metadata    nur stat, genug um ein Verzeichnis zu durchlaufen
file-write*           anlegen, ändern, löschen, umbenennen
process-exec          ein Programm starten
process-fork          forken
network-outbound      hinausverbinden
mach-lookup           einen Systemdienst über seinen Namen erreichen
sysctl-read           Kernel-Parameter lesen
signal                ein Signal senden

Filter grenzen eine Operation auf ein Ziel ein:

(literal "/etc/hosts")              genau dieser Pfad
(subpath "/Users/du/Projekte")      dieser Ordner und alles darunter
(regex #"^/Users/du/\\.config/")    ein Muster, selbst zu verankern
(global-name "com.apple.system.notification_center")
(remote ip "*:443")

Apples eigene Profile liegen in /System/Library/Sandbox/Profiles/ und lassen sich mit (import "bsd.sb") hereinholen. Sie sind lesbar, sie sind die beste Dokumentation, die es gibt, und sie ändern sich zwischen Versionen - was der Grund ist, sie zu lesen statt sich auf sie zu verlassen.

Eine Ablehnung finden

Ein abgelehnter Zugriff meldet sich nicht. Das Programm scheitert einfach, meistens mit einer Meldung über eine fehlende Datei. Die Ablehnung selbst geht ins Unified Log:

log stream --style compact --predicate 'senderImagePath CONTAINS "Sandbox"'

Jede Zeile nennt Prozess, Operation und Pfad:

Sandbox: node(51234) deny(1) file-read-data /Users/du/.npmrc

Lass das in einem zweiten Fenster mitlaufen, während du dein Programm startest, und das Profil schreibt sich von selbst. (deny default (with no-log)) beruhigt den Strom wieder, wenn du fertig bist, und (trace "/tmp/out.sb") lässt die Sandbox ein Profil aus allem aufzeichnen, was der Prozess versucht hat - der schnellste Weg zu einem ersten Entwurf und nie etwas, das man ungelesen ausliefert.

Die Fallen

Pfade werden aufgelöst, bevor sie verglichen werden. /tmp ist ein Symlink auf /private/tmp, /etc auf /private/etc. Eine Regel für /tmp trifft nichts. Dein Home-Ordner kann auf einem System mit versiegeltem Volume unter /System/Volumes/Data/Users/du liegen, und derselbe Pfad existiert dann zweimal.

Eine Datei zu lesen braucht Metadaten auf den Ordnern darüber. (subpath ...) erledigt das innerhalb des Teilbaums, eine (literal ...)-Regel für eine einzelne tief liegende Datei meistens nicht, und der Fehler sieht aus, als fehlte die Datei.

Sandboxen lassen sich nicht schachteln. Innerhalb der ersten lässt sich keine zweite starten, was auffällt, sobald deine Sitzung ein Werkzeug aufrufen will, das selbst etwas einschließt.

Schreiben ist nicht alles, was eine App beim Start tut. Caches, Lock-Dateien, Absturzberichte, ein Mach-Dienst, den die App zu erreichen erwartet. Gerade Programme mit Fenster fassen mehrere Dutzend Pfade an, bevor sie etwas zeichnen, und Electron-Apps legen ihren eigenen Satz obendrauf.

sandbox-exec ist als veraltet markiert. Die Man-Page sagt das seit über zehn Jahren, und ausgeliefert wird es in jeder Version. Der Mechanismus darunter verschwindet nicht - er trägt den App Store - aber an der Kommandozeilen-Fassung hängt kein Versprechen.

Wohin das führt

Nichts davon ist schwer. Es ist nur lang. Ein Profil, mit dem eine Shell läuft, sind fünfzehn Zeilen; ein Profil, mit dem dein Editor, dein Agent, dein Paketmanager und deine Toolchain laufen, ohne dich dreimal am Tag zu überraschen, ist ein Projekt, und es fällt wieder an, sobald sich eines davon ändert.

Genau das hat BX übernommen: dieselbe Sandbox des Systems, das Profil zusammengesetzt aus Ordnern, die du anhakst, und aus Regeldateien im .gitignore-Format, dazu eine Ansicht der abgelehnten Zugriffe, damit ein Fehlschlag ein Klick ist und nicht eine Stunde im Log.

Wenn du lieber auf der Kommandozeile bleibst: bx dry <mode> --sbpl gibt das fertige Profil so aus, wie das System es liest, und von da an bist du wieder allein unterwegs.

Passend dazu: Claude Code in eine Sandbox stecken, KI-Werkzeuge von deinen Zugangsdaten fernhalten, BX im Vergleich.