Skip to content

Security Model

hypnguyen1209 edited this page Aug 25, 2026 · 1 revision

Security Model

Read this before pointing Codex Free at anything sensitive. This page explains what a tool call can reach, what bounds it, and — importantly — what does not bound it.


The one thing to internalize

The command allowlist is a guardrail against accidents, not a sandbox. It catches a model reaching for curl or rm -rf. It does not contain a determined one.

The defaults already include node, python, and cargo, each of which runs arbitrary code — node -e "..." can do anything the server process can. Shell redirection and explicit absolute/parent paths can reach outside the active project root even though each command starts with that root as its cwd. Multi-project selection isolates Codex Free's structured tools and per-conversation state; it is not an OS sandbox.

Treat everything below as reachable

...by whoever is authorized to use the configured connector or external endpoint:

  • everything in the active project root, read and write;
  • in multi-project mode, any project beneath the access root can be selected by a new conversation or unbound transport;
  • anything else the user account running the server can touch, via an allowlisted interpreter;
  • the network, from your machine;
  • anything a bridged MCP server can do.

The practical rules that follow from this:

  • Don't point Codex Free at directories you don't trust the model with.
  • Set exec.mode and the command allowlists tighter than the defaults when the work directory is sensitive.
  • In multi-project mode, treat the whole access-root subtree as sensitive — it's all intentionally selectable.
  • For an external tunnel, require tunnel-level access control, not URL secrecy.

What is enforced

Path traversal prevention

Every filesystem tool — including apply_patch and view_image — resolves paths through a guard that rejects anything outside the active project root. In multi-project mode, both catalogue discovery and set_project_root canonicalize the access root and candidate, so .. and symlinks cannot expose or bind a project outside it.

The one bounded read-above---work-dir exception

In single-project mode only, AGENTS.md discovery may read above --work-dir, up to the nearest .git. It is read-only, opens only AGENTS.override.md / AGENTS.md / projectDoc.fallbackFilenames, and get_project_doc reports the absolute path of every file used. Switch it off with projectDoc.maxBytes: 0, or keep the search inside the work dir with projectDoc.rootMarkers: []. Multi-project mode does not perform this walk.

Command allowlist

run_command only runs binaries in allowedCommands. exec_command checks the same list plus exec.extraAllowedCommands, at every command position in the string (after every |, &&, ;, newline, subshell). ls | curl evil.com is rejected on curl. Command substitution ($(...), backticks) is rejected outright. Set exec.mode: "unrestricted" only when you fully trust the caller.

Host-authorized native-file ingress

import_host_file accepts only ChatGPT's declared native-file object (no arbitrary URLs), constrains the download and every redirect hop to artifactIngress.allowedHosts (default "*" admits any public HTTPS host but never loopback/private/link-local/CGNAT/metadata), ignores ambient proxy credentials, and enforces whole-request/idle/size/concurrency limits. Its signed URL and file ID are never logged or returned. Publication uses a capability-confined directory handle, SHA-256, a private partial, and atomic no-overwrite linking, so traversal/symlink/partial-visibility/replacement races fail closed.

Bounded state and reads outside the work dir

remember/update_plan write memory.json under ~/.codex-free/projects/; multi-project also writes a binding record under ~/.codex-free/conversation-projects/ (filename hashed from openai/session; raw value never stored). Skills may be read from ~/.agents/skills, ~/.codex/skills, ~/.claude/skills, or a Claude Code plugin — but skills_read is confined to a skill package that already exists; the resource path can't walk out into the rest of your home directory.

Namespaced review state

Review checkpoints are exactly two refs per conversation/project under refs/codex-free/review/, built through a temporary index, never modifying the real index/working tree. See Review Checkpoints.

Read-only Codex config discovery

MCP import and the project catalogue read the user-level Codex config.toml without rewriting it. Native Codex trust does not override the access-root boundary.


Native tunnel security properties

  • Outbound-only. Codex Free binds its MCP listener to loopback and supervises OpenAI's official runtime-only tunnel client. Startup fails unless the runtime reports /readyz and completes a control-plane poll. Failure of either process stops the other.
  • Authenticated loopback hop. Native mode generates a random per-process bearer token and configures the tunnel runtime to send it on MCP requests and discovery probes. The token is never printed, written to config, or inherited by model-launched commands or bridged children.
  • Verified installation. The managed client is pinned to a specific official release and per-platform SHA-256 embedded in Codex Free, extracted by exact filename under size limits, installed atomically with private permissions, and hash-checked against its manifest on later starts. Set clientPath to opt out of managed installation while keeping compatibility checks.
  • Secrets are references, not values. openaiTunnel.apiKeyRef accepts only env:NAME or file:/path; literal keys are rejected. Codex Free resolves the value and exposes it to the tunnel child under a synthetic env name, giving the child a clean allowlisted environment; it removes the original variable from model-launched commands and bridged children. On Unix, a referenced key file must not be group/other-readable. Use a restricted runtime key (Tunnels Read + Use), not an admin key.
  • Forced hardening. Native mode ignores allowedHosts, forces accepted authorities to 127.0.0.1/localhost/::1, binds only loopback, and disables permissive CORS. It cannot be combined with --api-key.

These measures prevent accidental inheritance and public exposure. They do not create a secret boundary against hostile code running as the same OS user, which can potentially inspect same-user processes or read an accessible key file.

Non-native / external mode

  • Bearer token: --api-key requires Authorization: Bearer <key> on all requests except /health. (ChatGPT's URL-connector auth may not support simple bearer tokens.)
  • Host allowlist: without openaiTunnel, allowedHosts is empty (accepts any Host). Set it to a hostname list for DNS-rebinding protection — only matching Host headers are served.
  • You must put an authenticated proxy/tunnel in front and enforce access at that layer. An unprotected public URL is not an appropriate long-lived deployment.

Bridged servers

An explicit mcpServers entry or an imported Codex MCP — including plugin-contributed ones — forwards the model's calls verbatim with full delegated authority. Keep the surface small: prefer tools/disabledTools filters or gateway mode, keep secrets in bearerTokenEnvVar/envHttpHeaders rather than static JSON, and disable auto-import (codexMcp.useCli: false or codexMcp.enabled: false) if you don't want it. See Bridging MCP Servers.

The worktree setup-script gate

worktrees.allowSetupScript defaults to false. Enabling it lets a worktree's Codex setup script run an arbitrary command outside the allowlist, with the script path selectable from the source repo's Git config — so an untrusted project could plant one. Leave it off unless every reachable project is trusted. See Worktree Isolation.

Process cleanup

For clients without stable ChatGPT metadata, exec_command sessions are killed when the MCP transport closes. ChatGPT-owned sessions survive transport replacement and are killed by exec.idleTimeoutMs or shutdown. The kill includes children: taskkill /T /F on Windows, per-session process groups on POSIX. A process that deliberately re-parents/daemonizes still escapes — check for strays if a run leaves something listening.

Audit logging

--audit writes hashes, timings, result sizes, and redacted argument shape — not source, paths, credentials, or output. It is an operational log, not a tamper-evident boundary (model-launched commands run as the same user and could modify it). Keep it outside the access root. See Audit Logging.


See also

Clone this wiki locally