security: add macos worker seatbelt profiles

Jaime Fournier <jaimef@linbsd.org>

c2a59a9ecb665177f6600bc649fa5653ce12a862

diff --git a/docs/aproc.md b/docs/aproc.md
index d59a73b..dffa897 100644
--- a/docs/aproc.md
+++ b/docs/aproc.md
@@ -70,6 +70,7 @@ rlimits:   '((resource soft hard) …) ; native pre-exec setrlimit triples
 landlock-rules: '((read-only "/path") …)
 landlock-net-connect?: #t          ; deny TCP connect except allowed ports
 landlock-net-connect-ports: '(18080 …)
+seatbelt-profile: "(version 1)…"   ; macOS SBPL profile, pre-exec
 ```
 
 `rlimits:` is supported only on the argv/native path and is rejected with
@@ -91,6 +92,13 @@ port-scoped because that is the Landlock ABI boundary; pair them with
 `(std security worker)` `sandbox: '(fs exec net)` so missing platform support
 is reported as a structured fail-closed refusal.
 
+`seatbelt-profile:` is argv/native-only and is rejected with `pty: #t`. On
+macOS, the native child calls `sandbox_init` with the supplied SBPL profile
+after fd and rlimit setup and before `exec`. Non-macOS platforms reject it in
+the pre-exec hook. Higher-level code should prefer `(std security worker)`
+`sandbox: '(fs net)` so unsupported or degraded axes remain visible in
+structured diagnostics.
+
 ### Low-level handle
 
 ```
diff --git a/docs/ffi-audit.md b/docs/ffi-audit.md
index fc7fcb0..d88e7ad 100644
--- a/docs/ffi-audit.md
+++ b/docs/ffi-audit.md
@@ -40,8 +40,8 @@ Latest summary from `tools/ffi-audit-report.ss`:
     (rust-file-count 29)
     (c-file-count 5)
     (no-mangle-exports 196)
-    (unsafe-sites 385)
-    (unsafe-sites-with-nearby-safety-comment 385)
+    (unsafe-sites 390)
+    (unsafe-sites-with-nearby-safety-comment 390)
     (unsafe-sites-without-nearby-safety-comment 0)
     (unsafe-comment-review-sites 0)
     (export-review-sites 196))
@@ -80,9 +80,10 @@ function body no longer grants implicit permission for unsafe operations. Each
 unsafe operation still has to live inside an explicit `unsafe { ... }` block.
 On 2026-07-28, the `aproc.rs` and `seccomp.rs` unsafe sites covering the
 worker pre-exec launch path, fd handoff/cleanup, FFI buffer copies, rlimit
-decoding, wait4 outputs, seccomp `prctl` calls, and Linux Landlock pre-exec
-ruleset/path/net syscalls gained nearby `SAFETY:` invariant comments. The
-`crypto.rs` digest, HMAC, random, constant-time
+decoding, wait4 outputs, seccomp `prctl` calls, Linux Landlock pre-exec
+ruleset/path/net syscalls, and macOS Seatbelt `sandbox_init`/error-buffer
+calls gained nearby `SAFETY:` invariant comments. The `crypto.rs` digest,
+HMAC, random, constant-time
 comparison, AEAD, scrypt, PBKDF2, and Argon2id FFI buffer sites also gained
 nearby `SAFETY:` invariant comments; the same pass added fail-closed
 nonzero-null input checks for HMAC and AEAD optional buffers and checked AEAD
@@ -141,9 +142,10 @@ handle-store locks into normal FFI errors, rejects null argument/result buffers
 with nonzero lengths, checks guest-memory offsets with overflow-safe helpers,
 documents socket-fd adoption and caller-buffer invariants adjacent to the
 audited unsafe sites, and has Rust regressions for invalid C ABI inputs.
-`aproc.rs` now documents inherited-fd slice, `pre_exec`, `openpty`, and Linux
-Landlock pre-exec ruleset/path/net syscall invariants adjacent to the audited
-worker-launch unsafe sites.
+`aproc.rs` now documents inherited-fd slice, `pre_exec`, `openpty`, Linux
+Landlock pre-exec ruleset/path/net syscall invariants, and macOS Seatbelt
+`sandbox_init`/error-buffer invariants adjacent to the audited worker-launch
+unsafe sites.
 `duckdb_native.rs` now
 funnels path, SQL, text, and blob inputs through checked C ABI slice helpers,
 converts poisoned database/statement/result store locks into normal FFI errors,
@@ -173,7 +175,7 @@ funnels hostname, path, PEM, pin, and read/write buffers through checked C ABI
 helpers, rejects null/nonzero and oversized buffers before slicing, rejects
 negative accepted fds before adopting them as `TcpStream`, and documents socket
 shutdown invariants used to break blocked TLS I/O. The generated inventory now
-reports 385 annotated native unsafe sites, 0 remaining unsafe review sites, and
+reports 390 annotated native unsafe sites, 0 remaining unsafe review sites, and
 0 Scheme blocking calls missing `__collect_safe`.
 
 Remaining work before closing K3-P1-01:
diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index 7a7f379..a1e1c02 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -146,7 +146,7 @@ never pattern-based:
   and the unsafe pattern unrepresentable — that's most of P0/P1 below.
 - **The FFI boundary is our `unsafe`.** 91 files under `lib/std/` mention
   `foreign-procedure`; the current [FFI audit](ffi-audit.md) tracks 196
-  native `#[no_mangle]` sites and 385 Rust unsafe sites. That boundary is
+  native `#[no_mangle]` sites and 390 Rust unsafe sites. That boundary is
   where a K3-class adversary will concentrate, because it is the only place
   memory-corruption patterns *exist* in a Jerboa application.
   [`Philosophy.md`](Philosophy.md) Principle 4 and open tension #5 both
@@ -157,9 +157,11 @@ never pattern-based:
   kill, output caps, audit-log start/end records, and fail-closed refused axes.
   Memory limits are installed through native pre-exec `setrlimit` on supported
   platforms and refuse before launch where unavailable.
+  macOS Seatbelt no-write/no-network profiles are also installed through
+  native pre-exec `sandbox_init` for supported worker axes.
   `sandbox-launch` still returns status 126 `pre-exec-refused`,
   `supervise-available?` returns `#f`, and the remaining P0-02 work is native
-  Seatbelt/Capsicum parity for the worker path.
+  Seatbelt path/exec parity and Capsicum parity for the worker path.
 
 ---
 
@@ -216,7 +218,7 @@ when" must be answerable from `dist/release-evidence/` in minutes.
 | Self-confinement (pledge/unveil-style `cage!`) | `(std security cage)` | exists; documented in the security module inventory | security-reference §12 |
 | Privilege-separation pipe channels (no launcher) | `(std security privsep)` | channels only, by design | security-reference §5 |
 | Launch policy planner + egress policy objects | `(std os limits sandbox)` | passive planner remains `pre-exec-refused`; `(std os limits)` can emit rlimit triples for the worker/aproc backend | [limits.md](limits.md) |
-| Exec restricted worker facade | `(std security worker)` | exists; pure env, deadline, process-group kill, output caps, audit records, fail-closed refused axes, explicit `sandbox:` axis requests, egress proxy env wiring, memory rlimits installed pre-exec on supported platforms, and Linux syscall/ptrace seccomp plus Landlock filesystem/TCP-connect setup for requested axes; Seatbelt/Capsicum parity pending | security-reference §5 |
+| Exec restricted worker facade | `(std security worker)` | exists; pure env, deadline, process-group kill, output caps, audit records, fail-closed refused axes, explicit `sandbox:` axis requests, egress proxy env wiring, memory rlimits installed pre-exec on supported platforms, Linux syscall/ptrace seccomp plus Landlock filesystem/TCP-connect setup for requested axes, and macOS Seatbelt no-write/no-network setup for supported axes; Seatbelt path/exec parity and Capsicum parity pending | security-reference §5 |
 | Native async exec launcher (collect-safe) | `(std os aproc)` | exists — the primitive P0-02 should build on | [aproc.md](aproc.md) |
 | Parser hardening (depth/size/backtrack budgets: reader, JSON, XML, YAML, DNS, HTTP/2, WS, zlib, base64, hex, CSV, pregexp, format) | various | phases 1–4 done, 42 tests; phase 5 FFI audit inventory has per-site provisional verdicts, with remediation rules still open | security-reference §7; [ffi-audit.md](ffi-audit.md) |
 | Safe deserialization (tagged-JSON envelope, no native FASL on untrusted paths) | `(std safe-fasl)`, `(std fasl)` (trusted-only) | exists; raw `read`/FASL/`load` sites are classified by manifest and gated by `make audit` | [safety-guide.md](safety-guide.md) §10 |
@@ -267,7 +269,7 @@ Keep this section to baseline facts only.
 | Files under `lib/std/` mentioning `foreign-procedure` | **91** | `grep -rl foreign-procedure lib/std \| wc -l` |
 | Native `#[no_mangle]` sites in `jerboa-native-rs/src` | **196** | `make ffi-audit-report` |
 | Actual native C ABI exports after cfg filtering | **190** | `make native-export-review-check` |
-| Rust unsafe sites in `jerboa-native-rs/src` | **385** | `make ffi-audit-report` |
+| Rust unsafe sites in `jerboa-native-rs/src` | **390** | `make ffi-audit-report` |
 | Fuzz harnesses | 13 | `ls tests/fuzz/harness` |
 | Checked-in fuzz seed corpora | **0** | `tests/fuzz/` contains only `harness/` |
 | Security test files | 21 | Appendix A |
@@ -309,7 +311,7 @@ Keep this section to baseline facts only.
 | Goal | Where we stand | The gap |
 |---|---|---|
 | G1 shrink target | Managed core is memory-safe; parsers budgeted; safe prelude exists; FFI audit inventory, per-site provisional verdicts, FFI hazard scanner rules, a gated `vendor/jsqlite` accepted-risk decision, native unsafe invariant comments, and a gated native export review exist | FFI remediation remains: Scheme per-binding safety review |
-| G2 cap blast radius | Capabilities, taint, kernel sandbox, egress policy objects, worker facade, memory rlimit pre-exec path, egress proxy env wiring, Linux pre-exec seccomp/Landlock setup, authenticated actor transport/envelopes, Ring HTTP source taint, and WebSocket/fiber WebSocket source taint all exist | Native Seatbelt/Capsicum worker parity remains open |
+| G2 cap blast radius | Capabilities, taint, kernel sandbox, egress policy objects, worker facade, memory rlimit pre-exec path, egress proxy env wiring, Linux pre-exec seccomp/Landlock setup, macOS Seatbelt no-write/no-network setup, authenticated actor transport/envelopes, Ring HTTP source taint, and WebSocket/fiber WebSocket source taint all exist | Native Seatbelt path/exec parity and Capsicum worker parity remain open |
 | G3 find it first | 13 harnesses, scanner w/ rule DB, lint | No corpora, no crash regression, no scheduled fuzzing, no standing AI-red-team, no exploit-shaped regression suite |
 | G4 fail closed | Raw-fork launchers retired correctly; `allow-degraded?` explicit | New controls must keep the invariant; degraded-mode warnings must be test-locked |
 | G5 recover fast | SBOM/repro/signing gates exist | TCB accounting manual; doc drift (stale tables, undocumented modules); independent-builder reproducibility not yet routine |
@@ -355,8 +357,10 @@ wiring landed 2026-07-27. Native pre-exec syscall/ptrace seccomp setup for
 requested worker axes landed 2026-07-28 through `(std os aproc)`
 `seccomp-lock?:`. Native pre-exec Landlock filesystem/TCP-connect setup for
 requested worker axes landed 2026-07-28 through `(std os aproc)`
-`landlock-rules:` and `landlock-net-connect?:`. Seatbelt/Capsicum parity
-remains open.
+`landlock-rules:` and `landlock-net-connect?:`. Native pre-exec Seatbelt
+no-write/no-network setup for supported macOS worker axes landed 2026-07-28
+through `(std os aproc)` `seatbelt-profile:`. Seatbelt path/exec parity and
+Capsicum parity remain open.
 
 Every security doc routes adversarial work to "a bounded, separately exec'd
 worker". The initial facade exists; finish it as the assembly point for
@@ -688,8 +692,9 @@ the current inventory and remediation trail now live in [ffi-audit.md](ffi-audit
   with overflow-safe helpers, documents socket-fd adoption and caller-buffer
   invariants adjacent to the audited unsafe sites, and has Rust regressions
   for invalid C ABI inputs. `aproc.rs` now documents inherited-fd slice,
-  `pre_exec`, and `openpty` invariants adjacent to the audited
-  worker-launch unsafe sites. `duckdb_native.rs` now validates path, SQL, text,
+  `pre_exec`, `openpty`, Linux Landlock, and macOS Seatbelt invariants adjacent
+  to the audited worker-launch unsafe sites. `duckdb_native.rs` now validates
+  path, SQL, text,
   blob, result-slot, result-index, and output-buffer inputs through shared
   helpers and converts poisoned handle-store locks into normal FFI errors.
   `x509.rs` now validates
@@ -709,7 +714,7 @@ the current inventory and remediation trail now live in [ffi-audit.md](ffi-audit
   publication, and test-owned fd conversion invariants. `tls.rs` now validates
   hostname, path, PEM, pin, and read/write buffers through shared helpers,
   rejects invalid accepted fds before ownership transfer, and documents socket
-  shutdown invariants for blocked TLS I/O. The generated report now shows 385
+  shutdown invariants for blocked TLS I/O. The generated report now shows 390
   annotated native unsafe sites, 0 remaining unsafe review sites, and 0 Scheme
   blocking calls missing `__collect_safe`.
   Remaining work: finish per-binding Scheme FFI review and targeted scanner
@@ -869,9 +874,11 @@ Chez cannot heap-cap a thread; `run-safe-eval` rightly refuses
   and configured/available/installed state for seccomp, Landlock, Seatbelt,
   Capsicum, and memory limits. `(std security worker)` now routes requested
   `syscalls`/`ptrace` sandbox axes through native aproc pre-exec seccomp on
-  Linux, and routes requested `fs`/`exec`/`net` axes through native aproc
-  pre-exec Landlock filesystem and TCP-connect rules on Linux. Seatbelt,
-  Capsicum, and per-platform CI parity remain open.
+  Linux, routes requested `fs`/`exec`/`net` axes through native aproc pre-exec
+  Landlock filesystem and TCP-connect rules on Linux, and routes supported
+  `fs`/`net` axes through native aproc pre-exec Seatbelt no-write/no-network
+  profiles on macOS. Seatbelt path/exec parity, Capsicum, and per-platform CI
+  parity remain open.
 
 ### K3-P1-09 — Confused-deputy defenses: capability plans enforced at runtime
 **Serves:** G2. **Effort:** 1 week.
@@ -1202,7 +1209,7 @@ Track these in `docs/status.md` per release:
 | Metric | Baseline (2026-07-27) | Target |
 |---|---|---|
 | Build balance clean | `pattern.ss` repaired 2026-07-27; `source-balance` in `make audit` | always clean |
-| Confined worker exists | facade, audit lifecycle, output caps, deadline, process-group kill, memory rlimit pre-exec path, explicit sandbox-axis refusal, egress proxy env wiring, Linux syscall/ptrace seccomp pre-exec setup, and Linux Landlock filesystem/TCP-connect setup landed; Seatbelt/Capsicum parity pending | yes, tested (P0-02) |
+| Confined worker exists | facade, audit lifecycle, output caps, deadline, process-group kill, memory rlimit pre-exec path, explicit sandbox-axis refusal, egress proxy env wiring, Linux syscall/ptrace seccomp pre-exec setup, Linux Landlock filesystem/TCP-connect setup, and macOS Seatbelt no-write/no-network setup landed; Seatbelt path/exec parity and Capsicum parity pending | yes, tested (P0-02) |
 | Unclassified raw `read`/FASL/`load` sites | scanner-driven manifest classification gate closed 2026-07-27; trusted build, compiler, REPL/dev, and test surfaces are centrally justified by exact source line | 0 |
 | FFI bindings audited | scanner inventory and provisional verdicts in [ffi-audit.md](ffi-audit.md); native exports gated and justified | 100% with final verdicts |
 | Un-annotated Rust `unsafe` blocks | 0 unsafe review sites without nearby `SAFETY:` comments in [ffi-audit.md](ffi-audit.md) | 0 |
@@ -1221,7 +1228,7 @@ Track these in `docs/status.md` per release:
 ```bash
 # Posture facts used in this document (re-run to refresh):
 grep -rl 'foreign-procedure' lib/std | wc -l        # 91
-make ffi-audit-report                              # native no_mangle 196; unsafe 385
+make ffi-audit-report                              # native no_mangle 196; unsafe 390
 make native-export-review-check                    # actual exports 190
 ls tests/fuzz/harness                                # 13 harnesses
 ls vendor/                                           # ChezScheme, ChezScheme-patches, jsqlite
@@ -1299,9 +1306,11 @@ fake confidence happens.
 - The confined exec worker facade exists as `(std security worker)`, with
   audit-log lifecycle events and native pre-exec memory rlimits on supported
   platforms. It now exposes explicit `sandbox:` fail-closed axis requests and
-  `egress-policy:` proxy env wiring, and installs Linux seccomp plus Landlock
-  filesystem/TCP-connect controls before `exec` for requested axes. Native
-  Seatbelt/Capsicum worker parity remains pending (P0-02/P1-08).
+  `egress-policy:` proxy env wiring, installs Linux seccomp plus Landlock
+  filesystem/TCP-connect controls before `exec` for requested axes, and
+  installs macOS Seatbelt no-write/no-network profiles before `exec` for
+  supported axes. Native Seatbelt path/exec parity and Capsicum worker parity
+  remain pending (P0-02/P1-08).
 - The committed `pattern.ss` balance blocker named in P0-01 was repaired on
   2026-07-27. The `pipeline.ss` and `test-pipeline.ss` reports were traced to
   escaped-identifier false positives in the balance scanner; the scanner now
diff --git a/docs/reviews/2026-07-27-native-export-review.sexp b/docs/reviews/2026-07-27-native-export-review.sexp
index 1c27eed..9b24aaa 100644
--- a/docs/reviews/2026-07-27-native-export-review.sexp
+++ b/docs/reviews/2026-07-27-native-export-review.sexp
@@ -19,11 +19,11 @@
     (export (symbol "jerboa_antidebug_check_tracer") (file "jerboa-native-rs/src/antidebug.rs") (line 37) (scheme-callers 1 (caller (file "lib/std/os/antidebug.ss") (line 38))))
     (export (symbol "jerboa_antidebug_ptrace") (file "jerboa-native-rs/src/antidebug.rs") (line 8) (scheme-callers 1 (caller (file "lib/std/os/antidebug.ss") (line 36))))
     (export (symbol "jerboa_antidebug_timing_check") (file "jerboa-native-rs/src/antidebug.rs") (line 129) (scheme-callers 1 (caller (file "lib/std/os/antidebug.ss") (line 44))))
-    (export (symbol "jerboa_aproc_killpg") (file "jerboa-native-rs/src/aproc.rs") (line 987) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 158))))
-    (export (symbol "jerboa_aproc_set_nonblock") (file "jerboa-native-rs/src/aproc.rs") (line 957) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 154))))
-    (export (symbol "jerboa_aproc_spawn") (file "jerboa-native-rs/src/aproc.rs") (line 431) (scheme-callers 2 (caller (file "lib/std/os/aproc.ss") (line 127)) (caller (file "lib/std/os/aproc.ss") (line 145))))
-    (export (symbol "jerboa_aproc_spawn_pty") (file "jerboa-native-rs/src/aproc.rs") (line 687) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 145))))
-    (export (symbol "jerboa_aproc_wait4") (file "jerboa-native-rs/src/aproc.rs") (line 1003) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 162))))
+    (export (symbol "jerboa_aproc_killpg") (file "jerboa-native-rs/src/aproc.rs") (line 1083) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 159))))
+    (export (symbol "jerboa_aproc_set_nonblock") (file "jerboa-native-rs/src/aproc.rs") (line 1053) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 155))))
+    (export (symbol "jerboa_aproc_spawn") (file "jerboa-native-rs/src/aproc.rs") (line 506) (scheme-callers 2 (caller (file "lib/std/os/aproc.ss") (line 127)) (caller (file "lib/std/os/aproc.ss") (line 146))))
+    (export (symbol "jerboa_aproc_spawn_pty") (file "jerboa-native-rs/src/aproc.rs") (line 783) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 146))))
+    (export (symbol "jerboa_aproc_wait4") (file "jerboa-native-rs/src/aproc.rs") (line 1099) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 163))))
     (export (symbol "jerboa_argon2id_hash") (file "jerboa-native-rs/src/crypto.rs") (line 814) (scheme-callers 2 (caller (file "lib/std/crypto/native-rust.ss") (line 276)) (caller (file "lib/std/crypto/password.ss") (line 35))))
     (export (symbol "jerboa_argon2id_verify") (file "jerboa-native-rs/src/crypto.rs") (line 867) (scheme-callers 2 (caller (file "lib/std/crypto/native-rust.ss") (line 296)) (caller (file "lib/std/crypto/password.ss") (line 42))))
     (export (symbol "jerboa_chacha20_open") (file "jerboa-native-rs/src/crypto.rs") (line 606) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 204))))
@@ -82,7 +82,7 @@
     (export (symbol "jerboa_landlock_add_path_rule") (file "jerboa-native-rs/src/landlock.rs") (line 129) (scheme-callers 1 (caller (file "lib/std/os/landlock-native.ss") (line 83))))
     (export (symbol "jerboa_landlock_create_ruleset") (file "jerboa-native-rs/src/landlock.rs") (line 97) (scheme-callers 1 (caller (file "lib/std/os/landlock-native.ss") (line 80))))
     (export (symbol "jerboa_landlock_enforce") (file "jerboa-native-rs/src/landlock.rs") (line 276) (scheme-callers 1 (caller (file "lib/std/os/landlock-native.ss") (line 89))))
-    (export (symbol "jerboa_last_error") (file "jerboa-native-rs/src/panic.rs") (line 9) (scheme-callers 11 (caller (file "lib/std/compress/native-rust.ss") (line 42)) (caller (file "lib/std/crypto/native-rust.ss") (line 48)) (caller (file "lib/std/crypto/x509.ss") (line 24)) (caller (file "lib/std/native.ss") (line 56)) (caller (file "lib/std/net/tls-rustls.ss") (line 167)) (caller (file "lib/std/os/aproc.ss") (line 167)) (caller (file "lib/std/os/integrity.ss") (line 42)) (caller (file "lib/std/os/secure-output.ss") (line 47)) (caller (file "lib/std/pcap.ss") (line 66)) (caller (file "lib/std/regex-native.ss") (line 51)) (caller (file "lib/std/wasm/sandbox.ss") (line 196))))
+    (export (symbol "jerboa_last_error") (file "jerboa-native-rs/src/panic.rs") (line 9) (scheme-callers 11 (caller (file "lib/std/compress/native-rust.ss") (line 42)) (caller (file "lib/std/crypto/native-rust.ss") (line 48)) (caller (file "lib/std/crypto/x509.ss") (line 24)) (caller (file "lib/std/native.ss") (line 56)) (caller (file "lib/std/net/tls-rustls.ss") (line 167)) (caller (file "lib/std/os/aproc.ss") (line 168)) (caller (file "lib/std/os/integrity.ss") (line 42)) (caller (file "lib/std/os/secure-output.ss") (line 47)) (caller (file "lib/std/pcap.ss") (line 66)) (caller (file "lib/std/regex-native.ss") (line 51)) (caller (file "lib/std/wasm/sandbox.ss") (line 196))))
     (export (symbol "jerboa_pbkdf2_derive") (file "jerboa-native-rs/src/crypto.rs") (line 740) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 246))))
     (export (symbol "jerboa_pbkdf2_verify") (file "jerboa-native-rs/src/crypto.rs") (line 773) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 261))))
     (export (symbol "jerboa_pcap_close") (file "jerboa-native-rs/src/pcap_capture.rs") (line 341) (scheme-callers 1 (caller (file "lib/std/pcap.ss") (line 58))))
diff --git a/docs/security-reference.md b/docs/security-reference.md
index 48dd8d0..0d94c5e 100644
--- a/docs/security-reference.md
+++ b/docs/security-reference.md
@@ -16,7 +16,7 @@ Jerboa's security model is layered defense-in-depth. No single layer is trusted 
 | **Taint tracking** | Mark untrusted data, reject at dangerous sinks | `(std security taint)` |
 | **Kernel enforcement** | Landlock filesystem rules, seccomp-BPF syscall filtering | `(std security landlock)`, `(std security seccomp)` |
 | **Privilege separation** | Pipe channels for separately exec'd supervisor/worker processes | `(std security privsep)` |
-| **Exec worker** | Run restricted eval in a fresh argv-exec'd process with pure env, deadline, output caps, supported memory rlimits, Linux seccomp, and Linux Landlock | `(std security worker)` |
+| **Exec worker** | Run restricted eval in a fresh argv-exec'd process with pure env, deadline, output caps, supported memory rlimits, Linux seccomp/Landlock, and macOS Seatbelt no-write/no-network profiles | `(std security worker)` |
 | **Parser hardening** | Depth limits, size limits, backtracking budgets | Various (see section 7) |
 | **Crypto** | AEAD, CSPRNG, HMAC, KDF, timing-safe comparison, secure memory | `(std crypto ...)` |
 | **Input sanitization** | Context-aware escaping for HTML, SQL, paths, headers, URLs | `(std security sanitize)` |
@@ -500,6 +500,15 @@ Landlock TCP connect port; host allowlisting still belongs to the proxy because
 Landlock's network ABI is port-scoped. Diagnostics include
 `native-landlock-requested?` and `native-landlock-installed?`.
 
+When `sandbox:` includes `fs`, `net`, or `seatbelt` on macOS, the worker passes
+an SBPL profile to `(std os aproc)`. The native child installs Seatbelt with
+`sandbox_init` after fd and rlimit setup and before `exec`. The current worker
+profile uses reliable no-write enforcement for `fs`/`seatbelt` and no-network
+enforcement for `net` without an egress proxy. Seatbelt `exec` path confinement
+and proxy-specific network allowlisting remain degraded and therefore refuse
+under the default `fail-closed?: #t`. Diagnostics include
+`native-seatbelt-requested?` and `native-seatbelt-installed?`.
+
 Use `egress-policy:` with a `(std net allow-proxy)` policy when the worker
 should route HTTP-capable clients through the deny-default allow proxy:
 
@@ -546,8 +555,10 @@ Current limitation: the worker has a real exec boundary, pure environment,
 parent deadline, process-group kill through `aproc`, returned output caps,
 audit-log start/end records, memory rlimits on supported platforms, Linux
 syscall/ptrace seccomp setup, Linux Landlock filesystem and TCP-connect setup
-for requested axes, explicit fail-closed sandbox-axis requests, and allow-proxy
-environment wiring. Seatbelt/Capsicum parity remains tracked by the K3 handoff.
+for requested axes, macOS Seatbelt no-write/no-network setup for supported
+axes, explicit fail-closed sandbox-axis requests, and allow-proxy environment
+wiring. Seatbelt path/exec parity and Capsicum worker parity remain tracked by
+the K3 handoff.
 
 ---
 
diff --git a/docs/status.md b/docs/status.md
index d4bf667..dbb6a0e 100644
--- a/docs/status.md
+++ b/docs/status.md
@@ -14,7 +14,7 @@ release artifacts are built as Jerboa multicall binaries with `jerboa`,
 | CLI | `jerboa`, `jerboa run`, `jerboa eval`, `jerboa test`, `jerboa build`, and `jerboa pkg` are the documented entry points. |
 | Language | `(jerboa prelude)` is the default import for user code and resolves to the safe surface for direct scripts; use `--unsafe-prelude` or `(jerboa prelude unsafe)` only for explicit raw access. `.sls` files remain implementation internals. |
 | Standard library | `lib/std` and `lib/jerboa` contain 647 `.ss` modules at this snapshot. See [libraries.md](libraries.md) and [api-index.md](api-index.md). |
-| Security | Landlock, seccomp, capability plans, taint-aware safe-prelude sinks, restricted evaluation, safe FASL, the exec worker facade, memory rlimit pre-exec setup, worker syscall/ptrace seccomp setup on Linux, worker Landlock filesystem/TCP-connect setup on Linux, security regression tests, and release evidence are implemented. Seatbelt/Capsicum parity remains open hardening work. |
+| Security | Landlock, seccomp, capability plans, taint-aware safe-prelude sinks, restricted evaluation, safe FASL, the exec worker facade, memory rlimit pre-exec setup, worker syscall/ptrace seccomp setup on Linux, worker Landlock filesystem/TCP-connect setup on Linux, worker Seatbelt no-write/no-network setup on macOS, security regression tests, and release evidence are implemented. Seatbelt path/exec parity and Capsicum worker parity remain open hardening work. |
 | Native Rust | `libjerboa_native` backs crypto, secure memory, compression, regex, selected databases, OS integrations, packet capture, and rustls TLS. |
 | Packaging | `jpkg` is shipped through the Jerboa multicall binary and documented in [jpkg-guide.md](jpkg-guide.md). |
 | Build and release | `make binary` is the local non-Linux release build, `make docker-build` is the Linux release build, and `make release-evidence` collects audit evidence. |
@@ -27,7 +27,7 @@ release artifacts are built as Jerboa multicall binaries with `jerboa`,
 | Kimi security handoff | [kimi3-security-recommmendations.md](kimi3-security-recommmendations.md) is the backlog. Dated evidence and review manifests live under [reviews/](reviews/). | Keep new security evidence in dated review records and summarize the current release state here. |
 | FFI audit phase 5 | [ffi-audit.md](ffi-audit.md) records the scanner output, provisional Scheme binding verdicts, and native Rust export inventory. `make native-export-review-check` gates native export decisions. The worker-launch native path in `aproc.rs`/`seccomp.rs`, crypto FFI buffer path in `crypto.rs`, secure-memory region lifecycle in `secure_mem.rs`, secure filesystem capability boundary in `secure_fs.rs`, TLS native ABI in `tls.rs`, Ed25519/X25519 key-agreement buffers, compression buffers, HTTP parse/writev boundary, embed-crypto ABI, integrity ABI, regex-native ABI, process-control ABI, pcap ABI, inotify ABI, epoll/eventfd ABI, antidebug ABI, SOCKS5 server ABI, PostgreSQL native ABI, Landlock native ABI, wasmi sandbox ABI, SpiderMonkey WASM ABI, DuckDB native ABI, X.509 native ABI, and SQLite native ABI now have nearby `SAFETY:` comments or equivalent checked FFI invariants. Generated counts are at 0 unannotated native unsafe sites and 0 Scheme blocking bindings missing `__collect_safe`. | Finish per-binding Scheme FFI review and targeted scanner rules for the remaining provisional verdicts. |
 | Native Rust exports | The native export review now has 190 exported functions: 183 tracked Scheme references and 7 retained standalone C/binary helpers. The previous 35 no-Scheme-reference removal candidates no longer have C ABI export markers. | Re-run `make native-export-review-check` whenever adding or removing native exports. |
-| Confined worker | `(std security worker)` provides the facade, audit lifecycle, output caps, deadlines, process-group kill, memory rlimit pre-exec setup, Linux syscall/ptrace seccomp pre-exec setup, Linux Landlock filesystem/TCP-connect setup for requested axes, explicit sandbox-axis refusal, and egress proxy env wiring. | Keep Linux/macOS/FreeBSD parity tests current and wire native Seatbelt/Capsicum worker paths. |
+| Confined worker | `(std security worker)` provides the facade, audit lifecycle, output caps, deadlines, process-group kill, memory rlimit pre-exec setup, Linux syscall/ptrace seccomp pre-exec setup, Linux Landlock filesystem/TCP-connect setup for requested axes, macOS Seatbelt no-write/no-network setup for supported axes, explicit sandbox-axis refusal, and egress proxy env wiring. | Keep Linux/macOS/FreeBSD parity tests current; finish Seatbelt path/exec confinement and wire native Capsicum worker paths. |
 | Safe surface | Direct scripts default to the safe prelude; raw access requires `--unsafe-prelude` or `(jerboa prelude unsafe)`. | Continue moving risky APIs behind explicit unsafe imports as new modules land. |
 
 ## Compatibility Notes
diff --git a/jerboa-native-rs/src/aproc.rs b/jerboa-native-rs/src/aproc.rs
index bbb123d..8a7a21e 100644
--- a/jerboa-native-rs/src/aproc.rs
+++ b/jerboa-native-rs/src/aproc.rs
@@ -20,6 +20,7 @@ const FLAG_ENV_CLEAR: i32 = 1 << 2; // env_clear() before applying envp (replace
 const FLAG_SECCOMP_LOCK: i32 = 1 << 3; // install debug/syscall seccomp blocklist pre-exec
 const FLAG_LANDLOCK: i32 = 1 << 4; // install Landlock FS policy pre-exec
 const FLAG_LANDLOCK_NET_CONNECT: i32 = 1 << 5; // handle TCP connect with Landlock pre-exec
+const FLAG_SEATBELT: i32 = 1 << 6; // install macOS Seatbelt SBPL profile pre-exec
 
 // Result buffer layout (5 * i32, little-endian, native order):
 //   [0] = pid
@@ -158,6 +159,25 @@ fn parse_u64_list(
     Ok(out)
 }
 
+fn parse_optional_cstring(
+    buf: *const u8,
+    len: usize,
+    what: &str,
+) -> Result<Option<CString>, String> {
+    if len == 0 {
+        return Ok(None);
+    }
+    if buf.is_null() {
+        return Err(format!("{} buffer is null", what));
+    }
+    // Bytes are copied into an owned CString before returning.
+    // SAFETY: `buf` is non-null and readable for exactly `len` bytes.
+    let slice = unsafe { std::slice::from_raw_parts(buf, len) };
+    CString::new(slice)
+        .map(Some)
+        .map_err(|e| format!("{} contains NUL: {}", what, e))
+}
+
 #[derive(Debug)]
 #[allow(dead_code)]
 struct LandlockPolicy {
@@ -298,8 +318,8 @@ fn install_landlock_pre_exec(policy: &LandlockPolicy) -> std::io::Result<()> {
     }
 
     for (path, access_mask) in policy.fs_rules.iter() {
-        // SAFETY: `path` is an owned CString moved into the pre-exec closure and
-        // remains NUL-terminated and live while open(2) reads it.
+        // `path` is owned by the pre-exec closure.
+        // SAFETY: it remains NUL-terminated and live while open(2) reads it.
         let parent_fd =
             unsafe { libc::open(path.as_ptr() as *const _, libc::O_PATH | libc::O_CLOEXEC) };
         if parent_fd < 0 {
@@ -416,6 +436,61 @@ fn install_landlock_pre_exec(_policy: &LandlockPolicy) -> std::io::Result<()> {
     ))
 }
 
+#[cfg(target_os = "macos")]
+fn install_seatbelt_pre_exec(profile: &CStr) -> std::io::Result<()> {
+    // The profile is an input C string, flags are a 64-bit bitset, and
+    // failures return an owned diagnostic pointer.
+    // SAFETY: these declarations match macOS sandbox_init(3) and
+    // sandbox_free_error(3).
+    unsafe extern "C" {
+        // SAFETY: sandbox_init reads a NUL-terminated input profile and writes
+        // at most one diagnostic pointer to `errorbuf`.
+        fn sandbox_init(
+            profile: *const libc::c_char,
+            flags: u64,
+            errorbuf: *mut *mut libc::c_char,
+        ) -> libc::c_int;
+        // SAFETY: sandbox_free_error accepts only pointers returned through
+        // sandbox_init's errorbuf slot.
+        fn sandbox_free_error(errorbuf: *mut libc::c_char);
+    }
+
+    let mut errorbuf: *mut libc::c_char = std::ptr::null_mut();
+    // A flags value of 0 selects SBPL string mode rather than a named profile.
+    // SAFETY: `profile` is owned and NUL-terminated; `errorbuf` is writable.
+    let rc = unsafe { sandbox_init(profile.as_ptr(), 0, &mut errorbuf) };
+    if rc != 0 {
+        let message = if errorbuf.is_null() {
+            "sandbox_init failed".to_string()
+        } else {
+            // SAFETY: sandbox_init returns a NUL-terminated diagnostic string
+            // owned by the sandbox library on failure; copy it before freeing.
+            let message = unsafe { CStr::from_ptr(errorbuf) }
+                .to_string_lossy()
+                .into_owned();
+            // SAFETY: errorbuf was allocated by sandbox_init and must be
+            // released with sandbox_free_error exactly once.
+            unsafe {
+                sandbox_free_error(errorbuf);
+            }
+            message
+        };
+        return Err(std::io::Error::new(
+            std::io::ErrorKind::PermissionDenied,
+            message,
+        ));
+    }
+    Ok(())
+}
+
+#[cfg(not(target_os = "macos"))]
+fn install_seatbelt_pre_exec(_profile: &CStr) -> std::io::Result<()> {
+    Err(std::io::Error::new(
+        std::io::ErrorKind::Unsupported,
+        "Seatbelt is only available on macOS",
+    ))
+}
+
 /// argv-style spawn via std::process::Command (posix_spawn fast path or
 /// fork+execvp). Avoids /bin/sh -c, so the caller does not need to escape
 /// args and there is no extra shell process in the tree.
@@ -457,6 +532,8 @@ pub extern "C" fn jerboa_aproc_spawn(
     landlock_net_port_buf: *const u8,
     landlock_net_port_buf_len: usize,
     landlock_net_port_count: usize,
+    seatbelt_profile_buf: *const u8,
+    seatbelt_profile_len: usize,
     flags: i32,
     result: *mut i32,
 ) -> i32 {
@@ -549,6 +626,7 @@ pub extern "C" fn jerboa_aproc_spawn(
         let seccomp_lock = (flags & FLAG_SECCOMP_LOCK) != 0;
         let landlock_enabled = (flags & FLAG_LANDLOCK) != 0;
         let landlock_net_connect = (flags & FLAG_LANDLOCK_NET_CONNECT) != 0;
+        let seatbelt_enabled = (flags & FLAG_SEATBELT) != 0;
 
         // Copy inherit-fd pairs into a Vec we move into the pre_exec closure
         // — the raw pointer + count would not satisfy `'static` bounds.
@@ -593,6 +671,21 @@ pub extern "C" fn jerboa_aproc_spawn(
         } else {
             None
         };
+        let seatbelt_profile = if seatbelt_enabled {
+            match parse_optional_cstring(seatbelt_profile_buf, seatbelt_profile_len, "seatbelt") {
+                Ok(Some(profile)) => Some(profile),
+                Ok(None) => {
+                    set_last_error("seatbelt profile is empty".into());
+                    return -1;
+                }
+                Err(e) => {
+                    set_last_error(format!("seatbelt parse: {}", e));
+                    return -1;
+                }
+            }
+        } else {
+            None
+        };
 
         // CommandExt::pre_exec runs this closure in the child after fork and
         // before exec.
@@ -634,6 +727,9 @@ pub extern "C" fn jerboa_aproc_spawn(
                 if let Some(policy) = landlock_policy.as_ref() {
                     install_landlock_pre_exec(policy)?;
                 }
+                if let Some(profile) = seatbelt_profile.as_ref() {
+                    install_seatbelt_pre_exec(profile.as_c_str())?;
+                }
                 if seccomp_lock {
                     install_seccomp_blocklist_pre_exec()?;
                 }
@@ -1108,4 +1204,15 @@ mod tests {
         )
         .is_err());
     }
+
+    #[test]
+    fn seatbelt_profile_rejects_null_nonempty_buffer() {
+        assert!(parse_optional_cstring(std::ptr::null(), 1, "seatbelt").is_err());
+    }
+
+    #[test]
+    fn seatbelt_profile_rejects_interior_nul() {
+        let profile = b"(version 1)\0(allow default)";
+        assert!(parse_optional_cstring(profile.as_ptr(), profile.len(), "seatbelt").is_err());
+    }
 }
diff --git a/lib/std/os/aproc.ss b/lib/std/os/aproc.ss
index e3e93d7..b9499f2 100644
--- a/lib/std/os/aproc.ss
+++ b/lib/std/os/aproc.ss
@@ -136,7 +136,8 @@
               u8* size_t size_t   ; Landlock path strings, len, count
               u8* size_t          ; Landlock access masks (u64 per path), len
               u8* size_t size_t   ; Landlock TCP connect ports (u64), len, count
-              int                 ; flags (pgroup/env/seccomp/landlock)
+              u8* size_t          ; Seatbelt SBPL profile, len
+              int                 ; flags (pgroup/env/seccomp/landlock/seatbelt)
               u8*)                ; result buf (5*int)
              int)
          (catch (e) #f))))
@@ -325,6 +326,7 @@
   (def FLAG_SECCOMP_LOCK 8)
   (def FLAG_LANDLOCK 16)
   (def FLAG_LANDLOCK_NET_CONNECT 32)
+  (def FLAG_SEATBELT 64)
 
   (def LANDLOCK_ACCESS_FS_EXECUTE #x1)
   (def LANDLOCK_ACCESS_FS_WRITE_FILE #x2)
@@ -470,7 +472,8 @@
           [seccomp-lock? #f]
           [landlock-rules '()]
           [landlock-net-connect? #f]
-          [landlock-net-connect-ports '()])
+          [landlock-net-connect-ports '()]
+          [seatbelt-profile #f])
       (let loop ([kw kwargs])
         (cond
           [(null? kw) (void)]
@@ -494,6 +497,12 @@
                [(landlock-net-connect?:) (set! landlock-net-connect? (and v #t))]
                [(landlock-net-connect-ports:)
                 (set! landlock-net-connect-ports (validate-landlock-ports v))]
+               [(seatbelt-profile:)
+                (unless (or (not v) (and (string? v) (> (string-length v) 0)))
+                  (raise-parse-error 'aproc
+                    "seatbelt-profile: expected non-empty string or #f, got ~s"
+                    v))
+                (set! seatbelt-profile v)]
                [else (error 'aproc-spawn* "unknown keyword" k)])
              (loop (cddr kw)))]))
       (when (and pty? (pair? rlimits))
@@ -503,6 +512,8 @@
       (when (and pty? (or (pair? (normalize-landlock-rules landlock-rules))
                           landlock-net-connect?))
         (error 'aproc-spawn* "Landlock options are not supported with pty: #t"))
+      (when (and pty? seatbelt-profile)
+        (error 'aproc-spawn* "seatbelt-profile: is not supported with pty: #t"))
       ;; Pack argv
       (let*-values
         ([(argv-buf argv-len argv-count) (pack-strings argv)]
@@ -539,13 +550,18 @@
                [rlimit-count (length rlimits)]
                [landlock-port-buf (pack-u64-list 'landlock-net-connect-ports
                                                   landlock-net-connect-ports)]
+               [seatbelt-profile-bv (if seatbelt-profile
+                                        (string->utf8 seatbelt-profile)
+                                        (make-bytevector 0))]
+               [seatbelt-profile-len (bytevector-length seatbelt-profile-bv)]
                [flags (bitwise-ior
                         (if new-pgroup? FLAG_NEW_PGROUP 0)
                         (if use-envp FLAG_USE_ENVP 0)
                         (if env-clear? FLAG_ENV_CLEAR 0)
                         (if seccomp-lock? FLAG_SECCOMP_LOCK 0)
                         (if (> landlock-path-count 0) FLAG_LANDLOCK 0)
-                        (if landlock-net-connect? FLAG_LANDLOCK_NET_CONNECT 0))]
+                        (if landlock-net-connect? FLAG_LANDLOCK_NET_CONNECT 0)
+                        (if seatbelt-profile FLAG_SEATBELT 0))]
                [result-buf (make-bytevector 20 0)])
           (let ([rc (if pty?
                       (call-pty-spawn argv-buf argv-len argv-count
@@ -565,6 +581,7 @@
                         landlock-access-buf (bytevector-length landlock-access-buf)
                         landlock-port-buf (bytevector-length landlock-port-buf)
                         (length landlock-net-connect-ports)
+                        seatbelt-profile-bv seatbelt-profile-len
                         flags result-buf))])
             (when (< rc 0)
               (error 'aproc-spawn*
@@ -1006,7 +1023,8 @@
           [seccomp-lock? (kw-ref kwargs 'seccomp-lock?:)]
           [landlock-rules (kw-ref kwargs 'landlock-rules:)]
           [landlock-net-connect? (kw-ref kwargs 'landlock-net-connect?:)]
-          [landlock-net-connect-ports (kw-ref kwargs 'landlock-net-connect-ports:)])
+          [landlock-net-connect-ports (kw-ref kwargs 'landlock-net-connect-ports:)]
+          [seatbelt-profile (kw-ref kwargs 'seatbelt-profile:)])
       (when (and seccomp-lock? (not c-jerboa-aproc-spawn))
         (error 'aproc-run/status* "seccomp-lock?: #t requires libjerboa_native"))
       ;; Build spawn kwargs
@@ -1028,6 +1046,9 @@
                              '())
                          (if landlock-net-connect-ports
                              (list 'landlock-net-connect-ports: landlock-net-connect-ports)
+                             '())
+                         (if seatbelt-profile
+                             (list 'seatbelt-profile: seatbelt-profile)
                              '()))]
              [h (cond
                   [c-jerboa-aproc-spawn
diff --git a/lib/std/security/worker.ss b/lib/std/security/worker.ss
index e79540b..d1c13e1 100644
--- a/lib/std/security/worker.ss
+++ b/lib/std/security/worker.ss
@@ -68,7 +68,9 @@
                 env-policy-default
                 env-policy-build
                 env-policy-validate-command
-                env-policy-audit-summary))
+                env-policy-audit-summary)
+          (only (std security seatbelt)
+                seatbelt-available?))
 
   (defstruct %worker-policy
     (command env-policy cwd timeout-ms memory-limit-bytes
@@ -118,7 +120,7 @@
 
   (def (absolute-path path)
     (cond
-      [(or (not path) (string=? path "")) path]
+      [(or (not path) (= (string-length path) 0)) path]
       [(char=? (string-ref path 0) #\/) path]
       [else
        (let ([cwd (current-directory)])
@@ -156,10 +158,10 @@
                                stdout-cap-bytes stderr-cap-bytes
                                require fail-closed?)]
           [(null? (cdr xs))
-           (error 'worker-policy "dangling keyword" (car xs))]
+           (error 'worker-policy "dangling option" (car xs))]
           [else
-           (let ([key (car xs)] [val (cadr xs)])
-             (case key
+           (let ([opt (car xs)] [val (cadr xs)])
+             (case opt
                [(command:) (set! command val)]
                [(env-policy:) (set! env-pol val)]
                [(cwd:) (set! cwd val)]
@@ -185,7 +187,7 @@
                [(stderr-cap-bytes:) (set! stderr-cap-bytes val)]
                [(require:) (set! require val)]
                [(fail-closed?:) (set! fail-closed? (and val #t))]
-               [else (error 'worker-policy "unknown keyword" key)])
+               [else (error 'worker-policy "unknown option" opt)])
              (loop (cddr xs)))]))))
 
   (def (valid-command? argv)
@@ -239,7 +241,13 @@
         '()))
 
   (def (worker-sandbox-refused-axes pol caps)
-    (missing-required-axes (worker-policy-sandbox pol) caps))
+    (let lp ([xs (worker-policy-sandbox pol)] [out '()])
+      (cond
+        [(null? xs) (reverse out)]
+        [(or (axis-installed? (car xs) caps)
+             (worker-native-satisfies-sandbox-axis? pol (car xs)))
+         (lp (cdr xs) out)]
+        [else (lp (cdr xs) (cons (car xs) out))])))
 
   (def (worker-native-seccomp-requested? pol)
     (let ([axes (worker-policy-sandbox pol)])
@@ -257,11 +265,12 @@
         '()))
 
   (def (worker-native-landlock-requested? pol)
-    (let ([axes (worker-policy-sandbox pol)])
-      (or (memq 'fs axes)
-          (memq 'exec axes)
-          (memq 'net axes)
-          (memq 'landlock axes))))
+    (and (eq? (sandbox-backend) 'landlock)
+         (let ([axes (worker-policy-sandbox pol)])
+           (or (memq 'fs axes)
+               (memq 'exec axes)
+               (memq 'net axes)
+               (memq 'landlock axes)))))
 
   (def (worker-native-landlock-available?)
     (and (aproc-native-spawn-available?)
@@ -314,11 +323,14 @@
            [repo-root (absolute-path ".")]
            [runtime-read (list cwd repo-root
                                (absolute-path "lib")
+                               (absolute-path ".chez/lib")
                                (absolute-path "vendor/jsqlite/src")
                                (absolute-path "support/security-worker-main.ss")
                                "/usr/lib" "/usr/share" "/lib" "/lib64"
                                "/etc/ssl" "/etc/resolv.conf" "/dev/urandom")]
-           [runtime-exec (list cmd-dir (absolute-path ".chez/bin")
+           [runtime-exec (list cmd-dir
+                               (absolute-path ".chez/bin")
+                               (absolute-path ".chez/lib")
                                "/bin" "/usr/bin")])
       (if (worker-native-landlock-requested? pol)
           (append (if (or (memq 'fs axes) (memq 'landlock axes))
@@ -365,6 +377,61 @@
       (if (and (integer? port) (>= port 0) (<= port 65535))
           (list port)
           '())))
+
+  (def (worker-native-seatbelt-supported-axis? pol axis)
+    (case axis
+      [(fs seatbelt) #t]
+      [(net) (not (worker-policy-egress-policy pol))]
+      [else #f]))
+
+  (def (worker-native-seatbelt-requested? pol)
+    (and (eq? (sandbox-backend) 'seatbelt)
+         (let lp ([xs (worker-policy-sandbox pol)])
+           (cond
+             [(null? xs) #f]
+             [(worker-native-seatbelt-supported-axis? pol (car xs)) #t]
+             [else (lp (cdr xs))]))))
+
+  (def (worker-native-seatbelt-available?)
+    (and (aproc-native-spawn-available?)
+         (eq? (sandbox-backend) 'seatbelt)
+         (seatbelt-available?)))
+
+  (def (worker-native-seatbelt-refused-axes pol)
+    (if (and (eq? (sandbox-backend) 'seatbelt)
+             (let lp ([xs (worker-policy-sandbox pol)])
+               (cond
+                 [(null? xs) #f]
+                 [(memq (car xs) '(fs exec net seatbelt)) #t]
+                 [else (lp (cdr xs))])))
+        (let lp ([xs '(fs exec net seatbelt)] [out '()])
+          (cond
+            [(null? xs) (reverse out)]
+            [(and (memq (car xs) (worker-policy-sandbox pol))
+                  (not (and (worker-native-seatbelt-available?)
+                            (worker-native-seatbelt-supported-axis? pol (car xs)))))
+             (lp (cdr xs) (cons (car xs) out))]
+            [else (lp (cdr xs) out)]))
+        '()))
+
+  (def (worker-native-satisfies-sandbox-axis? pol axis)
+    (or (and (worker-native-landlock-available?)
+             (memq axis '(fs exec net landlock)))
+        (and (worker-native-seatbelt-available?)
+             (worker-native-seatbelt-supported-axis? pol axis))))
+
+  (def (worker-seatbelt-profile pol command)
+    (if (worker-native-seatbelt-requested? pol)
+        (let ([axes (worker-policy-sandbox pol)])
+          (string-append
+           "(version 1)(allow default)"
+           (if (or (memq 'fs axes) (memq 'seatbelt axes))
+               "(deny file-write*)"
+               "")
+           (if (memq 'net axes)
+               "(deny network*)"
+               "")))
+        #f))
   (def (egress-policy-summary pol)
     (let ([egress (worker-policy-egress-policy pol)])
       (if egress
@@ -372,17 +439,17 @@
           (allowlist . ,(allow-proxy-allowlist egress)))
         'disabled)))
 
-  (def (remove-env-keys env keys)
+  (def (remove-env-names env names)
     (let lp ([xs env] [out '()])
       (cond
         [(null? xs) (reverse out)]
         [(and (pair? (car xs))
               (let ([name (caar xs)])
-                (let kp ([ks keys])
+                (let np ([ns names])
                   (cond
-                    [(null? ks) #f]
-                    [(string=? name (car ks)) #t]
-                    [else (kp (cdr ks))]))))
+                    [(null? ns) #f]
+                    [(string=? name (car ns)) #t]
+                    [else (np (cdr ns))]))))
          (lp (cdr xs) out)]
         [else (lp (cdr xs) (cons (car xs) out))])))
 
@@ -390,8 +457,8 @@
     (let ([egress (worker-policy-egress-policy pol)])
       (if egress
         (let* ([proxy-env (allow-proxy-env egress)]
-               [keys (map car proxy-env)])
-          (append (remove-env-keys env keys) proxy-env))
+               [names (map car proxy-env)])
+          (append (remove-env-names env names) proxy-env))
         env)))
 
   (def (append-refused-axes a b)
@@ -425,6 +492,8 @@
        (native-seccomp-available? . ,(aproc-native-spawn-available?))
        (native-landlock-requested? . ,(and (worker-native-landlock-requested? pol) #t))
        (native-landlock-available? . ,(worker-native-landlock-available?))
+       (native-seatbelt-requested? . ,(and (worker-native-seatbelt-requested? pol) #t))
+       (native-seatbelt-available? . ,(worker-native-seatbelt-available?))
        (sandbox . ,(worker-policy-sandbox pol))
        (egress . ,(egress-policy-summary pol))
        (env . ,(env-policy-audit-summary (worker-policy-env-policy pol)))
@@ -433,8 +502,11 @@
 
   (def (run-aproc-captured command pol env stdin-data)
     (let* ([landlock-rules (worker-landlock-rules pol command)]
-           [landlock-net? (and (memq 'net (worker-policy-sandbox pol)) #t)]
+           [landlock-net? (and (worker-native-landlock-requested? pol)
+                               (memq 'net (worker-policy-sandbox pol))
+                               #t)]
            [landlock-ports (worker-landlock-net-ports pol)]
+           [seatbelt-profile (worker-seatbelt-profile pol command)]
            [args (append
                   (list 'env-pure: env)
                   (if (worker-policy-cwd pol)
@@ -458,6 +530,9 @@
                   (if (pair? landlock-ports)
                       (list 'landlock-net-connect-ports: landlock-ports)
                       '())
+                  (if seatbelt-profile
+                      (list 'seatbelt-profile: seatbelt-profile)
+                      '())
                   (list 'timeout-ms: (worker-policy-timeout-ms pol)
                         'encoding: 'utf8-lossy
                         'new-pgroup: #t))])
@@ -501,6 +576,9 @@
          (native-landlock-requested? . ,(and (worker-native-landlock-requested? pol) #t))
          (native-landlock-installed? . ,(and (worker-native-landlock-requested? pol)
                                              (worker-native-landlock-available?)))
+         (native-seatbelt-requested? . ,(and (worker-native-seatbelt-requested? pol) #t))
+         (native-seatbelt-installed? . ,(and (worker-native-seatbelt-requested? pol)
+                                            (worker-native-seatbelt-available?)))
          (sandbox . ,(worker-policy-sandbox pol))
          (egress . ,(egress-policy-summary pol))
          (stdout-truncated? . ,stdout-truncated?)
@@ -522,13 +600,16 @@
            [memory-missing (memory-limit-refused-axes pol)]
            [native-seccomp-missing (worker-native-seccomp-refused-axes pol)]
            [native-landlock-missing (worker-native-landlock-refused-axes pol)]
+           [native-seatbelt-missing (worker-native-seatbelt-refused-axes pol)]
            [missing (append-refused-axes
                      (append-refused-axes
                       (append-refused-axes
-                       (append-refused-axes required-missing sandbox-missing)
-                       memory-missing)
-                      native-seccomp-missing)
-                     native-landlock-missing)])
+                       (append-refused-axes
+                        (append-refused-axes required-missing sandbox-missing)
+                        memory-missing)
+                       native-seccomp-missing)
+                      native-landlock-missing)
+                     native-seatbelt-missing)])
       (cond
         [(and (worker-policy-fail-closed? pol) (pair? missing))
          (worker-refusal-result command pol caps missing 'required-axis-unavailable)]
diff --git a/tests/test-aproc.ss b/tests/test-aproc.ss
index 0b668f1..91745b6 100644
--- a/tests/test-aproc.ss
+++ b/tests/test-aproc.ss
@@ -113,6 +113,15 @@
     #f)
   (lambda (msg)
     (equal? msg "Landlock options are not supported with pty: #t")))
+(test-pred "seatbelt-profile: rejects pty"
+  (guard (exn [#t (and (message-condition? exn)
+                       (condition-message exn))])
+    (aproc-spawn* '("true")
+      'pty: #t
+      'seatbelt-profile: "(version 1)(deny default)")
+    #f)
+  (lambda (msg)
+    (equal? msg "seatbelt-profile: is not supported with pty: #t")))
 
 (test-pred "landlock-net-connect-ports: rejects invalid port"
   (guard (exn [#t (and (message-condition? exn)
diff --git a/tests/test-worker.ss b/tests/test-worker.ss
index 2e20ee5..eec078a 100644
--- a/tests/test-worker.ss
+++ b/tests/test-worker.ss
@@ -191,21 +191,23 @@
                (and (equal? (worker-result-status r) 126)
                     (pair? (worker-result-refused-axes r))))))))
 
-(test-pred "requested filesystem sandbox installs native Landlock or refuses"
+(test-pred "requested filesystem sandbox installs native kernel control or refuses"
   (worker-run-eval
    "(+ 1 1)"
    (worker-policy
     'command: worker-command
     'timeout-ms: 3000
-    'sandbox: '(fs exec)
+    'sandbox: '(fs)
     'fail-closed?: #t))
   (lambda (r)
     (and (worker-result? r)
          (let ([diag (worker-result-diagnostics r)])
            (if (worker-result-launched? r)
                (and (equal? (worker-result-status r) 0)
-                    (eq? (alist-ref/default diag 'native-landlock-requested? #f) #t)
-                    (eq? (alist-ref/default diag 'native-landlock-installed? #f) #t))
+                    (or (and (eq? (alist-ref/default diag 'native-landlock-requested? #f) #t)
+                             (eq? (alist-ref/default diag 'native-landlock-installed? #f) #t))
+                        (and (eq? (alist-ref/default diag 'native-seatbelt-requested? #f) #t)
+                             (eq? (alist-ref/default diag 'native-seatbelt-installed? #f) #t))))
                (and (equal? (worker-result-status r) 126)
                     (pair? (worker-result-refused-axes r))))))))