# Security model in Codex

Codex combines a sandbox (or permission profile), an approval policy and command rules in config.toml and .rules files, with requirements.toml for admin limits and opt-in OpenTelemetry for audit.

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

## At a glance

| | Codex |
|---|---|
| Term | Sandbox mode or permission profile, plus approval policy, plus command rules |
| Configured in | `.codex/config.toml` (project), `~/.codex/config.toml`, `.codex/rules/*.rules`, `requirements.toml` (admins) |
| Loads / runs | The OS sandbox limits spawned commands; the approval policy decides when Codex stops to ask; rules decide commands that need to leave the sandbox |
| Scope & precedence | CLI flags, project (trusted only), profile file, user, cloud-managed, system. Admin `requirements.toml` constrains all of them |

## Build the scenario

Two baselines for `ticket-service`: a developer's laptop, and the `agent-ready` CI job from [headless execution](https://ai-sw-factory.mellicci.dev/fundamentals/headless-execution/codex).

| | Developer baseline | CI baseline |
|---|---|---|
| Files | `.codex/config.toml`, `.codex/rules/ticket-service.rules` | Workflow `with:` inputs, same rules file |
| Reads | Sandbox allows them | Sandbox allows them |
| `pnpm test`, `pnpm lint` | `allow` rule | `allow` rule |
| Other commands, network | `on-request` prompts | `never`: no prompts; network stays off |
| `rm -rf`, `git push --force` | `forbidden` rules | Same rules file |
| `.env*`, migrations | Permission profile | Not enforceable by the sandbox (see below) |

Developer `.codex/config.toml` (Codex reads it only after you trust the project):

```toml
default_permissions = "ticket-dev"
approval_policy = "on-request"

[permissions.ticket-dev]
extends = ":workspace"

[permissions.ticket-dev.filesystem]
glob_scan_max_depth = 3

[permissions.ticket-dev.filesystem.":workspace_roots"]
"**/.env*" = "deny"
"src/db/migrations" = "read"
```

`.codex/rules/ticket-service.rules` (Starlark):

```python
prefix_rule(pattern = ["pnpm", ["test", "lint"]], decision = "allow")
prefix_rule(pattern = ["rm", "-rf"], decision = "forbidden",
            justification = "Delete specific files instead.")
prefix_rule(pattern = ["git", "push", "--force"], decision = "forbidden",
            justification = "Open a PR; never rewrite shared history.")
prefix_rule(pattern = ["git", "push", "-f"], decision = "forbidden")
```

Check a rule before relying on it: `codex execpolicy check --pretty --rules .codex/rules/ticket-service.rules -- git push --force`.

CI job: the `Run Codex` step from the headless page, with the network setting made explicit:

```yaml
      - uses: openai/codex-action@v1
        with:
          openai-api-key: ${{ secrets.OPENAI_API_KEY }}
          sandbox: workspace-write
          codex-args: >-
            ["-c","approval_policy=never",
             "-c","sandbox_workspace_write.network_access=false"]
          output-file: codex-output.md
```

| CI control | Where |
|---|---|
| Credential only | `OPENAI_API_KEY` passed to the Codex step only; the action's proxy keeps it from later steps |
| Token scopes | Job `permissions:` `contents: write`, `pull-requests: write`, `issues: read` |
| Only maintainers label | Action default: only users with write access can trigger; `allow-users` / `allow-bots` widen it |
| Issue text as data | Passed through environment variables, never interpolated into the shell |
| Logs kept | `output-file` uploaded as an artifact; `[otel]` export if you run a collector |

Why `.env*` and migrations are not in the CI column: the `sandbox` input sets `sandbox_mode`, and permission profiles do not combine with it (see below). The documentation doesn't show a way to protect paths under `workspace-write` other than the protected `.git`, `.agents` and `.codex`. In CI, keep secrets out of the checkout and review migration changes in the pull request.

## Specifics

### Permission modes

| Approval policy | Behavior |
|---|---|
| `on-request` | Default for interactive use. Sandboxed commands run; escalations prompt |
| `never` | No prompts; Codex works within the sandbox. The documented choice for non-interactive runs |
| `{ granular = { ... } }` | Keeps some prompt categories interactive and auto-rejects others: `sandbox_approval`, `rules`, `mcp_elicitations`, `request_permissions`, `skill_approval` |
| `untrusted` | Retired; it can stop Codex from starting. `on-failure` is deprecated |

| Sandbox | Effect |
|---|---|
| `read-only` | Read files, run commands that do not write |
| `workspace-write` | Writes in the workspace and `/tmp`; `.git`, `.agents`, `.codex` stay read-only; network off unless `sandbox_workspace_write.network_access = true` |
| `danger-full-access` | No sandbox. `--yolo` also removes approvals |

Permission profiles (beta) replace `sandbox_mode`: built-ins `:read-only`, `:workspace`, `:danger-full-access`, or a custom `[permissions.<name>]` with `extends`, `filesystem` (`read`, `write`, `deny`; `deny` wins) and `network` rules. Set either profiles or `sandbox_mode`, not both; if `sandbox_mode` or `--sandbox` is present, it wins. Domain rules apply only when `features.network_proxy = true` and network is on.

### Allow, ask and deny rules

| Field | Values |
|---|---|
| `pattern` | Required list; each element a literal or a list of alternatives |
| `decision` | `allow` (default), `prompt`, `forbidden`; the most restrictive match wins |
| `justification`, `match`, `not_match` | Optional; Codex checks the examples when it loads the file |

Rules are experimental, and the docs describe them as controlling which commands run outside the sandbox. The documentation doesn't specify whether a `forbidden` rule also stops a command the sandbox would allow. The match is an exact argument prefix: `git push origin --force` does not match `["git", "push", "--force"]`. Codex splits simple `bash -lc "a && b"` chains and checks each command; scripts with redirection, substitution or wildcards are checked as one command. Project rules load only from a trusted `.codex/` layer.

### Approvals and headless runs

| Situation | What happens |
|---|---|
| Interactive `on-request` | Escalations prompt you |
| `never` (CI) | Nothing prompts; Codex works within the sandbox. The docs don't spell out the outcome of a blocked escalation |
| `approvals_reviewer = "auto_review"` | A reviewer agent answers eligible prompts. It changes who reviews, not the sandbox; with `never` there is nothing to review |

### Managed and project policy

| Layer | Detail |
|---|---|
| Project trust | `.codex/` layers (config, hooks, rules) load only for trusted projects; `[projects."<path>"] trust_level = "untrusted"` in user config skips them |
| `requirements.toml` | `/etc/codex/requirements.toml`, cloud or MDM; constrains values, for example `allowed_approval_policies`, `allowed_sandbox_modes`, `allowed_permission_profiles` |
| Admin rules | `[rules] prefix_rules = [{ pattern = [{ token = "rm" }], decision = "forbidden" }]` |

### Untrusted input and audit

| Topic | Codex |
|---|---|
| Prompt injection | The action docs: sanitize issue, PR and commit text; review hidden text; run Codex as the last step of a job |
| Web search | `web_search = "cached"` by default; `"live"` and `"disabled"` are the alternatives |
| Audit | `[otel]` with `exporter = "otlp-http"` or `"otlp-grpc"`; off by default. Events include `codex.tool_decision` (approved or denied, by configuration or user) and `codex.tool_result`. Keep `log_user_prompt = false` unless policy allows |

## Gotchas

- Protecting files with a permission profile and setting `--sandbox` (or the action's `sandbox` input) are mutually exclusive; the older setting wins and the profile is ignored.
- A rule is an exact prefix. `rm -rf` does not catch `rm -fr` or `rm -r -f`; test variants with `codex execpolicy check`.
- Project `.codex/config.toml` and rules do nothing until the project is trusted. The documentation doesn't specify how trust is established in a fresh CI checkout.
- The `**/.env*` glob is adapted from the documented `**/*.env`; confirm it with `codex sandbox linux --permissions-profile ticket-dev`.
- Network off blocks `pnpm install` inside Codex; install dependencies in an earlier step.
