# OS-native sandbox

> How agentjail-shield wraps your agent in the kernel sandbox so shell tricks and subprocess spawns can't bypass policy.

`agentjail-shield` wraps your coding agent in the operating system's kernel
sandbox **before** exec'ing it. Every subprocess the agent spawns inherits the
restrictions, so tricks like `printf x > ~/.ssh/id_rsa`, `eval $(base64 -d)`,
or `python -c "open(...).write(...)"` all return `EPERM` at the kernel level,
regardless of any hook bypass.

This is the OS-native enforcement layer in agentjail's
[isolation model](/docs/concepts/isolation-tiers), stronger than hooks alone.

## Quick start

Run your agent through the sandbox with `agentjail claude` (or `agentjail run --
codex` / `agentjail run -- cursor`) - it shields by default:

```sh
agentjail claude
```

The shield is a role of the single `agentjail` binary. You can invoke it
directly on any command through the `agentjail-shield` symlink:

```sh
agentjail-shield -- sh -c "cat ~/.ssh/id_rsa"
# → Operation not permitted
```

To run hook-only, without the OS sandbox, add `--no-sandbox`
(`agentjail claude --no-sandbox`).

## How it works

### macOS: Apple Seatbelt

agentjail-shield generates an Apple Seatbelt (sbpl) profile from your policy
config and execs the agent under `sandbox-exec`. The profile is **deny-list based** (allow-by-default):

- **Denies writes** to sensitive paths: `~/.ssh`, `~/.aws`, `~/.gnupg`,
  `~/.config`, `~/.agentjail`, `~/.docker`, `~/.kube`, `~/.cargo`,
  `~/Library/Keychains`, `~/Downloads`, `~/Desktop`, `/etc`, `/var`
- **Denies writes** matching sensitive filenames: `.env*`, `*.pem`, `*.key`,
  `id_rsa`, `credentials`, `.netrc`, `~/.npmrc`, `~/.pypirc`,
  `~/.git-credentials`
- **Denies reads** of credential paths: `~/.ssh`, `~/.aws`, `~/.gnupg`,
  `~/.docker`, `~/.kube`, `~/Library/Keychains`, private key files
- **Allows reads** of system trust stores (`/private/etc/ssl`,
  `/System/Library/Keychains`) so TLS works
- **Restricts network egress** (see [Network enforcement](#network-enforcement))

No sudo, no entitlement, no Developer ID required. `sandbox-exec` ships on
every macOS since 10.5.

### Linux: Landlock LSM

On Linux, agentjail-shield uses
[Landlock](https://docs.kernel.org/userspace-api/landlock.html) (available
since Linux 5.13). Landlock is **allowlist-based**, the opposite of the macOS
deny-list:

| Allowed (read-write) | Allowed (read-only) | Denied |
| --- | --- | --- |
| `/tmp`, current working directory | `/usr`, `/bin`, `/lib`, `/lib64`, `/sbin`, `/etc`, `/dev`, `/proc`, `/sys`, `/opt`, `/run`, `$HOME` (excluding sensitive subdirs) | Everything else |

Sensitive subdirectories (`~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.agentjail`,
`~/.config`) are never added to the allowlist, so they are denied by default.

Landlock restrictions are **irreversible**: once applied, neither the process
nor its descendants can lift them. No special privileges required.

### Other platforms

On unsupported platforms (Windows, FreeBSD, etc.), agentjail-shield prints a
warning and execs the agent **without** any sandbox (fail-open). The hook layer
(`agentjail-hook`) still runs on every tool call.

## Network capture and enforcement

The shield captures the agent's network traffic and checks it against
`network.allowed_hosts`. This works on **both Linux and macOS**.

### Capture gateway (default)

The LLM provider call (`POST /v1/messages`) is captured by default through a
base-URL capture gateway - on macOS this needs **no system extension**, because
the shield points the agent's own `ANTHROPIC_BASE_URL` at a per-session loopback
proxy that records the request/response and forwards to the real provider over
TLS.

### Transparent tunnel (opt in with `--tunnel`)

To capture and enforce *all* of the agent's traffic - not just the provider call
- opt into the transparent tunnel with `--tunnel`. It routes everything through
agentjail's gVisor + MITM engine (network namespaces on Linux, a system
extension on macOS) and enforces `network.allowed_hosts`, with HTTP/2 and gRPC
support.

Run `agentjail doctor` to see the effective value of every network knob and
where it came from (CLI flag > env var > `policy.yaml` > default, per ADR 0110).

### `--netproxy` (deprecated)

The older egress firewall - `agentjail-netproxy`, a localhost CONNECT proxy the
agent's `HTTPS_PROXY` points at - still works but is deprecated in favor of the
tunnel.

## Relationship to the hook layer

The sandbox does **not** replace the hook (`agentjail-hook` + `agentjail-daemon`).
They serve complementary roles:

| Capability | Hook | Sandbox |
| --- | --- | --- |
| MCP server allowlisting | Yes | No |
| Command-intent rules (`git push --force`) | Yes | No |
| Tell the agent *why* something was blocked | Yes | No |
| UX decisions (allow / deny / ask) | Yes | No (deny only) |
| Catch shell/eval/Python file writes | No | **Yes** (kernel-level) |
| Catch subprocess bypass | No | **Yes** (inherited by descendants) |
| Network per-host enforcement | No | **Yes** (via the tunnel, both platforms) |

Use both for defense in depth. The hook catches the 90% case with good UX; the
sandbox is the safety net that catches the rest.

## Configuration

The shield reads the same `~/.agentjail/policy.yaml` as the hook and daemon.
The relevant sections:

### `file.extra_deny`

Additional write-denied paths, appended to the built-in sensitive path list:

```yaml
file:
  extra_deny:
    - /Users/me/secret-project
    - /opt/production-data
```

### `file.extra_allow`

Additional write-allowed paths (Linux Landlock only; no effect on macOS):

```yaml
file:
  extra_allow:
    - /data/scratch
```

### `network.allowed_hosts`

Hosts the agent can reach, enforced by the transparent tunnel (both platforms):

```yaml
network:
  allowed_hosts:
    - api.github.com
    - registry.npmjs.org
    - "*.example.com"
```

Wildcards follow cert-style matching: `*.example.com` matches
`foo.example.com` but **not** `example.com` itself. Built-in defaults include
GitHub, npm, PyPI, crates.io, Go proxy, and Deno.

## CLI reference

```text
agentjail-shield [flags] -- <agent-cmd> [args...]
```

The `--` separator is **required**.

| Flag | Default | Description |
| --- | --- | --- |
| `--policy=PATH` | `~/.agentjail/policy.yaml` | Path to the policy config file |
| `--profile-print` | `false` | Print the generated sandbox profile to stderr and exit |
| `--tunnel` | `false` | Capture and enforce *all* traffic through the transparent tunnel |
| `--no-provider-gateway` | `false` | Disable the base-URL LLM capture gateway |
| `--no-sandbox` | `false` | Run hook-only, without the OS sandbox |

See `agentjail doctor` for the effective network configuration and precedence
(ADR 0110); `--netproxy` is deprecated in favor of `--tunnel`.

## Debugging

```sh
# Print the generated profile without running the agent
agentjail-shield --profile-print -- claude

# Test a specific operation
agentjail-shield -- sh -c "echo test > ~/.ssh/test_file"
# Expected: "Operation not permitted" on macOS
```

## Fail behavior

| Scenario | Behavior |
| --- | --- |
| `sandbox-exec` missing (macOS) | Fail-open with loud warning; hook still active |
| Landlock unsupported (Linux < 5.13) | Fail-open with loud warning |
| Landlock setup error (other) | Fail-closed unless `AGENTJAIL_SHIELD_ALLOW_UNSANDBOXED=1` |
| `policy.yaml` missing | Falls back to built-in defaults |
| `agentjail-netproxy` not found | Falls back to port-based filtering |
| Unsupported platform | Fail-open with warning |

## Next steps

- [Isolation tiers](/docs/concepts/isolation-tiers): where the sandbox fits in
  agentjail's layered enforcement model.
- [Configuration](/docs/reference/configuration): the full `policy.yaml`
  reference.
- [How it works](/docs/concepts/how-it-works): the hook layer that complements
  the sandbox.
