security: add worker capsicum helper hook

ober

e046f6419aa4f4640d87f99cf77a2ba717ede857

diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index a1e1c02..ea07f95 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -159,9 +159,12 @@ never pattern-based:
   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.
+  The standard FreeBSD worker-eval helper can enter Capsicum at process start,
+  before reading stdin or evaluating the supplied expression.
   `sandbox-launch` still returns status 126 `pre-exec-refused`,
   `supervise-available?` returns `#f`, and the remaining P0-02 work is native
-  Seatbelt path/exec parity and Capsicum parity for the worker path.
+  Seatbelt path/exec parity plus arbitrary-command/proxy-aware Capsicum parity
+  for the worker path.
 
 ---
 
@@ -218,7 +221,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, 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 |
+| 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, macOS Seatbelt no-write/no-network setup for supported axes, and standard FreeBSD worker-eval Capsicum entry before payload read/eval; Seatbelt path/exec parity plus arbitrary-command/proxy-aware 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 |
@@ -311,7 +314,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, 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 |
+| 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, standard FreeBSD worker-eval Capsicum entry, authenticated actor transport/envelopes, Ring HTTP source taint, and WebSocket/fiber WebSocket source taint all exist | Native Seatbelt path/exec parity and arbitrary-command/proxy-aware 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 |
@@ -360,7 +363,10 @@ requested worker axes landed 2026-07-28 through `(std os aproc)`
 `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.
+arbitrary-command/proxy-aware Capsicum parity remain open. The standard
+FreeBSD worker-eval helper now enters Capsicum before reading stdin or
+evaluating the supplied expression when `sandbox:` requests a supported
+Capsicum axis.
 
 Every security doc routes adversarial work to "a bounded, separately exec'd
 worker". The initial facade exists; finish it as the assembly point for
@@ -877,8 +883,11 @@ Chez cannot heap-cap a thread; `run-safe-eval` rightly refuses
   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.
+  profiles on macOS. On FreeBSD, the standard worker-eval helper enters
+  Capsicum before reading stdin or evaluating the supplied expression for
+  supported `fs`/`exec`/`net`/`capsicum` axes. Seatbelt path/exec parity,
+  arbitrary-command/proxy-aware Capsicum parity, and per-platform CI parity
+  remain open.
 
 ### K3-P1-09 — Confused-deputy defenses: capability plans enforced at runtime
 **Serves:** G2. **Effort:** 1 week.
@@ -1209,7 +1218,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, 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) |
+| 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, macOS Seatbelt no-write/no-network setup, and standard FreeBSD worker-eval Capsicum entry landed; Seatbelt path/exec parity and arbitrary-command/proxy-aware 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 |
@@ -1309,8 +1318,10 @@ fake confidence happens.
   `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).
+  supported axes, and enters Capsicum at startup in the standard FreeBSD
+  worker-eval helper before reading the payload or evaluating it. Native
+  Seatbelt path/exec parity and arbitrary-command/proxy-aware 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/security-reference.md b/docs/security-reference.md
index 0d94c5e..04159e3 100644
--- a/docs/security-reference.md
+++ b/docs/security-reference.md
@@ -509,6 +509,20 @@ 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?`.
 
+When `sandbox:` includes `fs`, `exec`, `net`, or `capsicum` on FreeBSD for the
+standard `worker-run-eval` helper, the parent marks the pure child environment
+with `JERBOA_WORKER_CAPSICUM=1`. The helper checks `capsicum-available?` and
+applies `capsicum-compute-only-preset` before reading stdin or evaluating the
+supplied expression; the preset narrows stdin to read/fstat and stdout/stderr
+to write/fstat before entering capability mode. This is intentionally limited
+to the standard helper command:
+arbitrary `worker-run-command` processes cannot truthfully install Capsicum
+after path-based `exec`, so those requests still refuse by default. Capsicum
+`net` also refuses when an egress proxy is configured because capability mode
+does not implement host allowlisting. Diagnostics include
+`native-capsicum-requested?`, `native-capsicum-installed?`, and
+`native-capsicum-helper?`.
+
 Use `egress-policy:` with a `(std net allow-proxy)` policy when the worker
 should route HTTP-capable clients through the deny-default allow proxy:
 
@@ -557,7 +571,9 @@ 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, 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
+wiring. The standard FreeBSD eval helper can enter Capsicum before reading the
+payload or evaluating it. Seatbelt path/exec parity, arbitrary-command
+Capsicum worker parity, and proxy-aware Capsicum networking remain tracked by
 the K3 handoff.
 
 ---
diff --git a/docs/status.md b/docs/status.md
index dbb6a0e..086062d 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, 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. |
+| 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, standard worker-eval Capsicum entry on FreeBSD, security regression tests, and release evidence are implemented. Seatbelt path/exec parity and arbitrary-command 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, 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. |
+| 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, standard worker-eval Capsicum entry on FreeBSD, explicit sandbox-axis refusal, and egress proxy env wiring. | Keep Linux/macOS/FreeBSD parity tests current; finish Seatbelt path/exec confinement and arbitrary-command/proxy-aware 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/lib/std/security/worker.ss b/lib/std/security/worker.ss
index d1c13e1..07386fb 100644
--- a/lib/std/security/worker.ss
+++ b/lib/std/security/worker.ss
@@ -70,7 +70,9 @@
                 env-policy-validate-command
                 env-policy-audit-summary)
           (only (std security seatbelt)
-                seatbelt-available?))
+                seatbelt-available?)
+          (only (std security capsicum)
+                capsicum-available?))
 
   (defstruct %worker-policy
     (command env-policy cwd timeout-ms memory-limit-bytes
@@ -240,12 +242,12 @@
         '(memory-limit)
         '()))
 
-  (def (worker-sandbox-refused-axes pol caps)
+  (def (worker-sandbox-refused-axes pol caps command)
     (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)))
+             (worker-native-satisfies-sandbox-axis? pol command (car xs)))
          (lp (cdr xs) out)]
         [else (lp (cdr xs) (cons (car xs) out))])))
 
@@ -414,11 +416,59 @@
             [else (lp (cdr xs) out)]))
         '()))
 
-  (def (worker-native-satisfies-sandbox-axis? pol axis)
+  (def (worker-security-helper-command? command)
+    (let ([helper (absolute-path "support/security-worker-main.ss")])
+      (let lp ([xs command])
+        (cond
+          [(null? xs) #f]
+          [(and (string? (car xs)) (string=? (car xs) helper)) #t]
+          [else (lp (cdr xs))]))))
+
+  (def (worker-native-capsicum-supported-axis? pol axis)
+    (case axis
+      [(fs exec capsicum) #t]
+      [(net) (not (worker-policy-egress-policy pol))]
+      [else #f]))
+
+  (def (worker-native-capsicum-requested? pol)
+    (and (eq? (sandbox-backend) 'capsicum)
+         (let lp ([xs (worker-policy-sandbox pol)])
+           (cond
+             [(null? xs) #f]
+             [(worker-native-capsicum-supported-axis? pol (car xs)) #t]
+             [else (lp (cdr xs))]))))
+
+  (def (worker-native-capsicum-available?)
+    (and (aproc-native-spawn-available?)
+         (eq? (sandbox-backend) 'capsicum)
+         (capsicum-available?)))
+
+  (def (worker-native-capsicum-refused-axes pol command)
+    (if (and (eq? (sandbox-backend) 'capsicum)
+             (let lp ([xs (worker-policy-sandbox pol)])
+               (cond
+                 [(null? xs) #f]
+                 [(memq (car xs) '(fs exec net capsicum)) #t]
+                 [else (lp (cdr xs))])))
+        (let lp ([xs '(fs exec net capsicum)] [out '()])
+          (cond
+            [(null? xs) (reverse out)]
+            [(and (memq (car xs) (worker-policy-sandbox pol))
+                  (not (and (worker-native-capsicum-available?)
+                            (worker-security-helper-command? command)
+                            (worker-native-capsicum-supported-axis? pol (car xs)))))
+             (lp (cdr xs) (cons (car xs) out))]
+            [else (lp (cdr xs) out)]))
+        '()))
+
+  (def (worker-native-satisfies-sandbox-axis? pol command 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))))
+             (worker-native-seatbelt-supported-axis? pol axis))
+        (and (worker-native-capsicum-available?)
+             (worker-security-helper-command? command)
+             (worker-native-capsicum-supported-axis? pol axis))))
 
   (def (worker-seatbelt-profile pol command)
     (if (worker-native-seatbelt-requested? pol)
@@ -461,6 +511,14 @@
           (append (remove-env-names env names) proxy-env))
         env)))
 
+  (def (worker-merge-capsicum-env env pol command)
+    (if (and (worker-native-capsicum-requested? pol)
+             (worker-native-capsicum-available?)
+             (worker-security-helper-command? command))
+        (append (remove-env-names env '("JERBOA_WORKER_CAPSICUM"))
+                '(("JERBOA_WORKER_CAPSICUM" . "1")))
+        env))
+
   (def (append-refused-axes a b)
     (let lp ([xs b] [out a])
       (cond
@@ -494,6 +552,9 @@
        (native-landlock-available? . ,(worker-native-landlock-available?))
        (native-seatbelt-requested? . ,(and (worker-native-seatbelt-requested? pol) #t))
        (native-seatbelt-available? . ,(worker-native-seatbelt-available?))
+       (native-capsicum-requested? . ,(and (worker-native-capsicum-requested? pol) #t))
+       (native-capsicum-available? . ,(worker-native-capsicum-available?))
+       (native-capsicum-helper? . ,(worker-security-helper-command? command))
        (sandbox . ,(worker-policy-sandbox pol))
        (egress . ,(egress-policy-summary pol))
        (env . ,(env-policy-audit-summary (worker-policy-env-policy pol)))
@@ -579,6 +640,11 @@
          (native-seatbelt-requested? . ,(and (worker-native-seatbelt-requested? pol) #t))
          (native-seatbelt-installed? . ,(and (worker-native-seatbelt-requested? pol)
                                             (worker-native-seatbelt-available?)))
+         (native-capsicum-requested? . ,(and (worker-native-capsicum-requested? pol) #t))
+         (native-capsicum-installed? . ,(and (worker-native-capsicum-requested? pol)
+                                            (worker-native-capsicum-available?)
+                                            (worker-security-helper-command? command)))
+         (native-capsicum-helper? . ,(worker-security-helper-command? command))
          (sandbox . ,(worker-policy-sandbox pol))
          (egress . ,(egress-policy-summary pol))
          (stdout-truncated? . ,stdout-truncated?)
@@ -596,27 +662,31 @@
       (error 'worker-run-command "expected env-policy" (worker-policy-env-policy pol)))
     (let* ([caps (sandbox-capabilities)]
            [required-missing (missing-required-axes (worker-policy-require pol) caps)]
-           [sandbox-missing (worker-sandbox-refused-axes pol caps)]
+           [sandbox-missing (worker-sandbox-refused-axes pol caps command)]
            [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)]
+           [native-capsicum-missing (worker-native-capsicum-refused-axes pol command)]
            [missing (append-refused-axes
                      (append-refused-axes
                       (append-refused-axes
                        (append-refused-axes
-                        (append-refused-axes required-missing sandbox-missing)
-                        memory-missing)
-                       native-seccomp-missing)
-                      native-landlock-missing)
-                     native-seatbelt-missing)])
+                        (append-refused-axes
+                         (append-refused-axes required-missing sandbox-missing)
+                         memory-missing)
+                        native-seccomp-missing)
+                       native-landlock-missing)
+                      native-seatbelt-missing)
+                     native-capsicum-missing)])
       (cond
         [(and (worker-policy-fail-closed? pol) (pair? missing))
          (worker-refusal-result command pol caps missing 'required-axis-unavailable)]
         [else
-         (let* ([env (worker-merge-egress-env
-                      (env-policy-build (worker-policy-env-policy pol) getenv)
-                      pol)]
+         (let* ([env0 (worker-merge-egress-env
+                       (env-policy-build (worker-policy-env-policy pol) getenv)
+                       pol)]
+                [env (worker-merge-capsicum-env env0 pol command)]
                 [validation (env-policy-validate-command
                              (worker-policy-env-policy pol) '() command)])
            (cond
diff --git a/support/security-worker-main.ss b/support/security-worker-main.ss
index 4956975..66aa9e6 100644
--- a/support/security-worker-main.ss
+++ b/support/security-worker-main.ss
@@ -7,7 +7,8 @@
 ;;; it at the process boundary.
 
 (import (jerboa prelude)
-        (std security sandbox))
+        (std security sandbox)
+        (std security capsicum))
 
 (def (read-port-all port)
   (let ([out (open-output-string)])
@@ -26,7 +27,15 @@
      (with-output-to-string
        (lambda () (display-condition exn)))]))
 
+(def (install-worker-capsicum-if-requested!)
+  (let ([requested (getenv "JERBOA_WORKER_CAPSICUM")])
+    (when (and requested (string=? requested "1"))
+      (unless (capsicum-available?)
+        (error 'security-worker "Capsicum requested but unavailable"))
+      (capsicum-apply-preset! (capsicum-compute-only-preset 1)))))
+
 (def (main)
+  (install-worker-capsicum-if-requested!)
   (let ([expr (read-port-all (current-input-port))])
     (try
       (let* ([captured (open-output-string)]
diff --git a/tests/test-worker.ss b/tests/test-worker.ss
index eec078a..6b9d5a1 100644
--- a/tests/test-worker.ss
+++ b/tests/test-worker.ss
@@ -211,6 +211,25 @@
                (and (equal? (worker-result-status r) 126)
                     (pair? (worker-result-refused-axes r))))))))
 
+(test-pred "requested capsicum sandbox installs helper hook or refuses"
+  (worker-run-eval
+   "(+ 1 1)"
+   (worker-policy
+    'command: worker-command
+    'timeout-ms: 3000
+    'sandbox: '(capsicum)
+    '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-capsicum-requested? #f) #t)
+                    (eq? (alist-ref/default diag 'native-capsicum-installed? #f) #t)
+                    (eq? (alist-ref/default diag 'native-capsicum-helper? #f) #t))
+               (and (equal? (worker-result-status r) 126)
+                    (pair? (worker-result-refused-axes r))))))))
+
 (test-pred "egress proxy policy reaches worker env"
   (worker-run-command
    '("/usr/bin/env")