# Sandboxing in Codex

Codex runs each command under an OS sandbox set by `sandbox_mode`, with network off by default, and the documentation suggests `danger-full-access` only inside a container that is itself the boundary.

Source: https://ai-sw-factory.mellicci.dev/fundamentals/sandboxing/codex

Scope: Codex CLI on your machine and inside a container. Cloud environments get one line under [Escape hatches and limits](#escape-hatches-and-limits).

## At a glance

| | Codex |
|---|---|
| Term | Sandbox (`sandbox_mode`), separate from the approval policy |
| Configured in | `~/.codex/config.toml` or trusted project `.codex/config.toml`; `--sandbox` flag; `/permissions` in the TUI |
| Loads / runs | Every command Codex spawns (`git`, package managers, test runners) inherits the sandbox |
| Scope & precedence | Flags and `--config` beat project config, which beats user config. Default in a version-controlled folder: `workspace-write` with `on-request` approvals |

## Build the scenario

| Step | What you write or run | Where |
|---|---|---|
| 1 | Install Codex during container creation | `postCreateCommand` |
| 2 | Set the sandbox, network and environment policy | `.codex/config.toml` |
| 3 | Pass the task token as an env variable, log in the model key once | `containerEnv`, terminal |
| 4 | Decide: keep the inner sandbox, or `danger-full-access` | See [Running inside a container](#running-inside-a-container) |

Devcontainer fragment (firewall script and compose service as in the scenario):

```json
{
  "name": "ticket-service",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspace",
  "postCreateCommand": "npm install -g @openai/codex",
  "postStartCommand": "sudo .devcontainer/init-firewall.sh",
  "containerEnv": { "GH_TOKEN": "${localEnv:TICKET_AGENT_TOKEN}" }
}
```

Codex config for the repo:

```toml
sandbox_mode = "workspace-write"
approval_policy = "on-request"

[sandbox_workspace_write]
network_access = true          # pnpm install, git push

[features.network_proxy]
enabled = true                 # enforce the domain rules
domains = { "registry.npmjs.org" = "allow", "github.com" = "allow", "api.openai.com" = "allow" }

[shell_environment_policy]
inherit = "all"
filters = { "OPENAI_*" = "exclude", "CODEX_*" = "exclude" }
```

The container firewall stays the hard boundary; the proxy rules are a second, inner layer.

## Specifics

### Built-in sandbox

| `sandbox_mode` | Effect |
|---|---|
| `read-only` | Inspect files; no edits or commands without approval |
| `workspace-write` | Read, edit in the workspace (current directory plus temp directories such as `/tmp`), run routine commands. Network off |
| `danger-full-access` | No sandbox: no filesystem or network boundary |

| Platform | Mechanism |
|---|---|
| macOS | Seatbelt, built in |
| Linux and WSL2 | `bubblewrap` (`bwrap`) plus `seccomp`; install the package first |
| Windows | Native sandbox, `[windows] sandbox = "elevated"` or `"unelevated"` |

The sandbox is one control and the approval policy (`on-request`, `never`) is another: the sandbox is what is technically possible, the policy is when Codex stops and asks. `/status` shows the workspace directories.

### Filesystem and network rules

| Rule | Behavior |
|---|---|
| Extra writable directories | `sandbox_workspace_write.writable_roots` (array) |
| Temp directories | `exclude_slash_tmp` and `exclude_tmpdir_env_var` remove `/tmp` and `$TMPDIR` from the writable roots |
| Protected paths | Inside every writable root, `.git` (and the directory a `gitdir:` pointer file names), `.agents` and `.codex` are read-only, recursively |
| Network | Off in `workspace-write` until `network_access = true` |
| Domain allowlist | `features.network_proxy` with `domains`; needs network on, and does not grant network by itself |
| Permission profiles (beta) | `default_permissions` and `[permissions.<name>]` with `filesystem` (`read`, `write`, `deny`; `deny` wins) and `network.domains`; cannot be combined with `sandbox_mode` |

With `domains` unset, the docs say no external destinations are allowed until you add `allow` rules; `deny` wins on conflicts.

### Running inside a container

| Situation | What the docs say |
|---|---|
| Linux sandbox blocked | "When you run Linux in a containerized environment such as Docker, the sandbox may not work if the host or container configuration blocks the namespace, setuid `bwrap`, or `seccomp` operations that Codex needs." |
| Container is the boundary | "If the container is your intended security boundary, run Codex with `--sandbox danger-full-access` inside the container so Codex does not try to create a second sandbox layer." |
| Keep the inner sandbox | "Keep Codex's Linux sandbox enabled if the Dev Container profile grants the capabilities needed for `bwrap` to create the inner sandbox." |

The docs' reference implementation (`.devcontainer/devcontainer.secure.json`, `Dockerfile.secure`, `init-firewall.sh`) installs Codex and `bubblewrap` and applies an allowlist firewall. Install options in a container: `npm install -g @openai/codex` or `curl -fsSL https://chatgpt.com/codex/install.sh | sh`. The documentation doesn't list which capabilities `bwrap` needs; test with `codex sandbox`.

### Credentials and secrets

| Setting | Effect |
|---|---|
| `shell_environment_policy.inherit` | `all` (default), `core` or `none`: which variables spawned commands receive |
| `filters` | Case-insensitive patterns with `*` and `?`; `"exclude"` removes, any `"include"` turns the rest into an allowlist |
| `ignore_default_excludes` | Default `true`: names containing `KEY`, `SECRET` or `TOKEN` are not removed automatically. Set `false` to remove them |
| `set` | Injects explicit values after exclusions |

Model login: `printenv OPENAI_API_KEY | codex login --with-api-key` stores it in `~/.codex/auth.json` (or the OS credential store; `cli_auth_credentials_store`). Keep `GH_TOKEN` scoped to the task and out of the repo.

### Escape hatches and limits

| Mechanism | Semantics |
|---|---|
| `--sandbox danger-full-access` | Sandbox off; approvals unchanged |
| `--dangerously-bypass-approvals-and-sandbox` (alias `--yolo`) | No sandbox and no approvals; web search defaults to live |
| `-a never` | No prompts; still bounded by the sandbox mode |
| Auto-review (`approvals_reviewer = "auto_review"`) | A reviewer agent handles approval requests at the boundary; the sandbox is unchanged |
| Unix socket allowlist (`dangerously_allow_all_unix_sockets`) | Bypasses socket allowlist; the docs cite Docker |
| Cloud environments | Agent phase is offline by default; internet access is enabled per environment |

## Gotchas

- Inside `workspace-write`, `.git` is read-only, so commits and pushes from a sandboxed command can fail. The documentation doesn't show a setting that reopens `.git`; use approval or a rule.
- The docs warn that with `danger-full-access` or `--yolo` in a container, a malicious project can exfiltrate anything available there, including Codex credentials. Use trusted repositories only.
- `network_proxy` with no `network_access` does nothing; with `network_access` and no proxy, network is unrestricted.
- By default, token-like variables still reach commands. Filter what the model key and other secrets should not expose.
- The reference firewall is a starting point; the docs recommend DNS rebinding and refresh protection if you rely on domain allowlisting.
