---
title: "Sandbox Rules - Workshop Docs"
description: "Sandbox Rules - Enterprise control plane for Santa. Manage rules, approvals, telemetry, and policies across your macOS fleet."
doc_version: "1"
last_updated: "2026-08-18"
canonical: "https://northpole.security/docs/workshop/rules/sandbox-rules"
---
# Sandbox Rules

A Sandbox rule is an [execution rule](https://northpole.security/docs/workshop/rules/execution-rules) whose policy is **Seatbelt**. Instead of allowing or blocking the binary outright, it requires the binary to be launched through `santactl sandbox` (abbreviated `santactl sb`), which applies a macOS Seatbelt sandbox profile carried by the rule before executing it.

:::info Requirements

Santa gained sandbox support in **2026.5**, but policies saved by Workshop use the `BINARY_PATH` parameter, which Santa only understands from **2026.6**. Every Seatbelt rule is therefore stamped with a minimum version of 2026.6, and hosts on anything older will not receive it.

The feature is currently in beta.

:::

## Overview

Seatbelt is the macOS sandbox. Profiles are written in **SBPL** (Sandbox Policy Language), a Scheme-like language that names the operations a process may perform and the arguments those operations are allowed to take.

A Sandbox rule pairs a normal execution rule identifier (CDHash, Signing ID, Team ID, Certificate or Binary hash) with an SBPL profile:

-   Running the binary directly is **blocked**, with the reason ”… (requires running under `santactl sandbox`)”.
-   Running it as `santactl sandbox <binary> [args…]` is **allowed**, with the profile applied to the process for its lifetime.
-   The sandbox is inherited by every child process, so a sandboxed shell cannot escape by spawning something else.

Santa verifies that the binary it is about to allow is the same one the sandbox was set up for, so the sandboxed launch cannot be handed off to a different binary. Seatbelt decisions are never cached — every execution is re-checked.

## Creating a Sandbox rule

Create the rule as usual and set **Policy** to **Seatbelt**. A seatbelt policy body is required — the rule cannot be saved without one.

CEL rules can also produce a sandbox decision: if the expression can return `SEATBELT`, the rule must carry a seatbelt policy too. See the [CEL Guide](https://northpole.security/docs/workshop/rules/cel-guide).

Workshop appends the following line to every saved policy, so the sandboxed binary is always permitted to execute itself:

```
(allow process-exec* (literal (param "BINARY_PATH")))
```

It is appended only once; re-saving a rule does not duplicate it.

## Simple mode

The **Simple** tab generates a complete, deny-by-default profile from a handful of toggles. It is the recommended starting point: the generated profile already contains the boilerplate a typical CLI tool needs (system library reads, TTY access, logging, DNS, CoreFoundation shared memory) that is tedious and easy to get wrong by hand.

Switching a toggle regenerates the whole profile. If the policy in the Advanced tab is not something Simple mode would have produced (because it was hand-edited), Workshop shows a warning: touching any control will overwrite it.

### Process

Control

Effect

**Allow child processes (fork/exec)**

Adds `(allow process-fork)` and `(allow process-exec)`. Required for anything that spawns subprocesses — shells, build tools, compilers. Leave it off to confine the binary to itself.

### File reads

Control

Effect

**Allow reading `$HOME`**

Adds `(subpath (param "HOME"))` to the read list. The always-denied paths below still apply.

**Additional read paths**

One `(subpath …)` entry per path, granting recursive read access to that directory tree. Accepts absolute paths, or `$HOME`/`$CWD`/`~` prefixes (`$HOME/Projects`, `$CWD/vendor`).

### File writes

Control

Effect

**Allow writing to the working directory (`$CWD`)**

Adds `(subpath (param "CWD"))` to the write list — the directory `santactl sandbox` was invoked from.

**Allow writing to temp directories**

Adds `/private/tmp`, `/private/var/tmp` and `/private/var/folders`.

**Additional write paths**

One recursive `(subpath …)` write entry per path. Same `$HOME`/`$CWD` prefix handling as reads.

### Blocked paths

Control

Effect

**Paths that cannot be read or written**

Emits `(deny file-read* file-write* …)` *after* the allow sections. Because SBPL is last-match-wins, these win over any allow above — use them to punch holes in a broad allow.

### Network

Control

Effect

**Allow HTTPS**

Outbound TCP to `*:443` plus UDP `*:443` for QUIC/HTTP3.

**Allow localhost (inbound, outbound, bind)**

Outbound TCP to `localhost:*`, plus `network-inbound` and `network-bind` on `localhost:*`. Needed for local dev servers and stdio bridges.

**Additional outbound TCP destinations**

A bare port (`8080`) becomes `*:8080`; a `host:port` value (`example.com:993`) is used verbatim.

Outbound UDP to port 53 (DNS) and Unix domain sockets are always allowed when any network access is granted.

### Always in the generated profile

Regardless of the toggles, Simple mode emits:

-   `(version 1)`, `(deny default)` and `(debug deny)` — deny everything not explicitly allowed, and log each denial.
-   Process introspection of self and children, `sysctl-read`, and the Mach services and POSIX shared-memory names CoreFoundation, logging, DNS and the security daemons need.
-   Read access to `/System`, `/Library`, `/usr`, `/bin`, `/sbin`, `/opt`, `/private/etc`, timezone and mds databases, plus `/dev/null`, `/dev/zero`, `/dev/random`, `/dev/urandom` and the TTY devices.
-   Write access to `/dev/null`, `/dev/tty` and the process’s TTY.
-   A `(deny … (with no-log))` block for Apple telemetry services, to keep the denial log readable.

And these paths are **always denied**, even if a toggle or custom path would otherwise cover them:

Denied for

Paths

Writes

`~/.zshrc`, `~/.zshenv`, `~/.zprofile`, `~/.bashrc`, `~/.bash_profile`, `~/.profile`, `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/Library/LaunchAgents`, `~/Library/Application Support/com.apple.sharedfilelist`

Reads

`~/.ssh`, `~/.aws/credentials`, `~/.gnupg`, `~/Library/Keychains`

## Advanced mode

The **Advanced** tab is a raw editor for the SBPL profile. Use it when Simple mode cannot express the policy you need. Anything you write here is stored verbatim (plus the appended `process-exec*` directive).

### Structure

```
(version 1)
(deny default)          ; default stance
(debug deny)            ; log every denial

(allow file-read*
       (subpath "/usr")
       (literal "/dev/urandom"))

(deny file-write*
       (subpath (string-append (param "HOME") "/.ssh")))
```

Key semantics:

-   `(version 1)` must be the first directive.
-   Set the default stance with `(deny default)` or `(allow default)`. Always prefer `(deny default)`.
-   **Last match wins.** A later `deny` overrides an earlier `allow` for the same operation and path, which is how the “Blocked paths” section in Simple mode works.
-   `;` starts a comment; `;;` is the convention for section headers.
-   Profiles must be self-contained. `(import …)` of system profiles is not supported here.

### Operations

An operation names the class of action being authorized. `*` suffixes match a family of related operations (`file-write*` covers create, unlink, mode changes and so on).

Operation

Covers

`default`

Everything not otherwise matched.

`file-read*`, `file-read-data`, `file-read-metadata`

Reading file contents, and `stat`\-style metadata access.

`file-write*`, `file-write-data`, `file-write-create`, `file-write-unlink`, `file-write-mode`, `file-write-owner`, `file-write-flags`

Modifying files: contents, creation, deletion, permissions.

`file-ioctl`

`ioctl(2)` on a file or device — needed for TTYs.

`file-mount`, `file-umount`

Mounting and unmounting filesystems.

`process-exec`, `process-exec*`

Executing a binary.

`process-fork`

Creating child processes.

`process-info*`, `process-info-pidinfo`, `process-info-pidfdinfo`, `process-info-setcontrol`

Inspecting or controlling processes.

`signal`

Sending signals.

`network-outbound`, `network-inbound`, `network-bind`

Connecting out, accepting in, binding a local address.

`system-socket`

Creating sockets (including routing sockets).

`mach-lookup`, `mach-register`

Looking up or registering Mach services (XPC).

`ipc-posix-shm*`, `ipc-posix-shm-read*`, `ipc-posix-shm-write-data`, `ipc-posix-shm-write-create`

POSIX shared memory — CoreFoundation requires several of these.

`ipc-posix-sem*`

POSIX semaphores.

`sysctl-read`, `sysctl-write`

Reading and writing `sysctl` values.

`iokit-open`, `iokit-get-properties`

Opening IOKit user clients and reading their properties.

`user-preference-read`, `user-preference-write`

`cfprefsd` preference domains.

`nvram*`

Reading and writing NVRAM variables.

`device-camera`, `device-microphone`

Capture devices.

`authorization-right-obtain`

Acquiring an Authorization Services right.

The authoritative operation set is defined by the OS and varies between macOS releases; an unrecognized operation causes `sandbox_init` to fail. Test profiles that use uncommon operations against the macOS versions you deploy to.

### Filters

Filters narrow which arguments an operation applies to. Multiple filters in one rule are OR’d — the rule matches if any of them do.

Filter

Matches

`(literal "/path")`

Exactly that path.

`(subpath "/path")`

That directory and everything beneath it.

`(regex #"^/dev/ttys[0-9]+$")`

Paths matching the regular expression. Note the `#"…"` literal syntax.

`(global-name "com.apple.logd")`

A Mach service in the global namespace (used with `mach-lookup`).

`(local-name "…")`

A Mach service in the local namespace.

`(ipc-posix-name "…")`, `(ipc-posix-name-regex #"…")`

POSIX shared memory / semaphore names.

`(iokit-user-client-class "IOHIDParamUserClient")`

An IOKit user client class (used with `iokit-open`).

`(remote tcp "host:port")`, `(remote udp "*:53")`

Outbound destination. `*` is allowed for either half.

`(local tcp "localhost:*")`

Local address, for `network-inbound` and `network-bind`.

`(remote unix-socket)`, `(local unix-socket)`

Unix domain sockets.

`(target self)`, `(target children)`, `(target others)`

The process a `signal` or `process-info` operation applies to.

`(vnode-type REGULAR-FILE)`

Restrict a file operation to a vnode type (`DIRECTORY`, `SYMLINK`, `CHARACTER-DEVICE`, …).

Filters can be combined with `(require-all …)`, `(require-any …)` and `(require-not …)`:

```
(allow file-write*
       (require-all (subpath (param "CWD"))
                    (require-not (regex #"\.git/"))))
```

### Modifiers

Modifier

Effect

`(with no-log)`

Suppress the log entry for this rule. Useful for noisy, expected denials.

`(with report)`

Log the violation even when the operation is allowed.

`(with send-signal SIGKILL)`

Kill the process on a matching denial instead of returning an error.

```
(deny mach-lookup (with no-log)
       (global-name "com.apple.diagnosticd"))
```

### Parameters

`santactl sandbox` supplies these parameters when it applies the profile. Reference them with `(param "NAME")`, and build paths beneath them with `(string-append …)`.

Parameter

Value

`BINARY_PATH`

Full resolved path of the binary being sandboxed.

`CWD`

The working directory `santactl sandbox` was invoked from.

`HOME`

The invoking uid’s home directory from the password database (so `/var/root` under `sudo`, not the inherited `$HOME`).

`TMPDIR`

The per-user Darwin temp directory.

`UID`

The invoking uid, as a decimal string.

```
(allow file-read*
       (subpath (string-append (param "HOME") "/Projects")))
```

Referencing a parameter that was not supplied makes `sandbox_init` fail. `CWD`, `HOME` and `TMPDIR` are omitted if the OS cannot resolve them.

## Using a Sandbox rule on the client

The canonical command is `santactl sandbox`:

```
$ santactl sandbox <command> [arguments...]
```

`santactl sb` is an alias for it, provided as a shorthand. The two are identical in every respect; this documentation uses the canonical `santactl sandbox` throughout.

```
$ santactl sb <command> [arguments...]   # identical to the above
```

`--` ends option parsing, if the command name would otherwise look like a flag.

-   The command may be a path (absolute or containing a `/`), or a bare name resolved against `PATH`. Only absolute `PATH` entries are searched.
-   Root is not required. The Santa daemon must be running.
-   Arguments after the command are passed through unchanged.
-   The sandbox is applied before `exec`, so it is in effect from the very first instruction of the target — and is inherited by all of its children.

Common failures:

Message

Cause

`command not found`

The command did not resolve to an executable regular file.

`No matching rule`

No execution rule matches the binary.

`Rule is not a seatbelt rule`

A rule matched, but it carries no seatbelt policy.

`sandbox_init failed: …`

The profile is not valid SBPL, or references an undefined parameter.

`Concurrent sandbox request rejected`

Another `santactl sandbox` request from the same process is already pending.

### Troubleshooting a profile

Profiles generated by Simple mode include `(debug deny)`, so every denial is logged. Watch them live on the host:

```
$ log stream --style compact --predicate '((processID == 0) AND (senderImagePath CONTAINS "/Sandbox")) OR (subsystem == "com.apple.sandbox.reporting")'
```

Each entry names the operation and the argument that was denied, which maps directly onto the operation/filter pair to add to the profile. Iterate in the Advanced tab until the denial log is clean, then roll the rule out.

## Best practices

-   **Start narrow.** Keep `(deny default)` and grant only what the tool actually needs; a profile that starts from `(allow default)` protects nothing.
-   **Prefer Simple mode.** The generated baseline handles the OS plumbing that is easy to get wrong. Drop to Advanced only for the parts Simple mode cannot express.
-   **Scope by code signing identity.** As with any execution rule, a CDHash, Signing ID or Team ID rule is far harder to sidestep than a binary path.
-   **Test on one host first.** A broken profile does not fall back to unsandboxed execution — the binary simply will not run.
-   **Watch the rollout.** Hosts on Santa older than 2026.6 will silently not receive the rule, so the binary continues to be evaluated by the rest of your policy.

## Sitemap

- [Home](https://northpole.security/index.md)
- [Workshop](https://northpole.security/workshop.md)
- [Santa](https://northpole.security/santa.md)
- [Features](https://northpole.security/features.md)
- [Cookbook](https://northpole.security/cookbook.md)
- [Docs](https://northpole.security/docs.md)
- [Blog](https://northpole.security/blog.md)
- [Glossary](https://northpole.security/glossary.md)
- [About](https://northpole.security/about.md)
- [Contact](https://northpole.security/contact.md)
