OpenAI Codex · Code Mode

Network and Credential Proxy: The Key the Model Never Sees

Outbound traffic has to clear several doors. The real token never enters the child process. What the model sees is a fake value and a 403.

Course goalAfter this lesson you can explain three things. How a domain is judged: deny first, then local, then the allowlist. The real token is swapped for a same-shape fake before spawn; on the way out the proxy swaps it back by host. A client that won’t take the proxy is pressed into the same funnel by the OS sandbox.
Try it first · A credentialed request through the proxy
Swap the host, swap the method: see which gate stops the request, and which step swaps the fake for the real value
Host
Method
Mode
First watch the fake become real. Then switch to POST, or let Go skip the proxy.
Client
This request
GET https://api.github.com/repos/you/app/issues
Authorization
Bearer github_pat_dmy8f3a2
Gate 1 · SandboxStandby
Only the proxy port is allowed. A direct loopback dies at this gate.
Gate 2 · DomainStandby
Deny first, then local or private, then the allowlist.
Gate 3 · MethodStandby
Limited only allows GET, HEAD, OPTIONS.
Gate 4 · CredentialsStandby
Only if the request header carries the fake does it swap in the broker’s real token.
Child envGH_TOKEN = github_pat_dmy8f3a2
Broker memoryThe real token is locked here
Model contextCannot see the real value
Hit Play to watch this request walk four gates, and see which step swaps the fake for the real value.
Logic trail · each animation step maps to a stretch of source
  1. Before spawning the command, swap the real token for a same-shape fakecredential_broker.rs L92
  2. The sandbox only allows the proxy port; a direct loopback ends hereseatbelt.rs L325
  3. Resolve the host; deny is judged first and always winsruntime.rs L553
  4. A local literal without an exact allow is blocked as a private addressruntime.rs L578
  5. A name that resolves off the public net is blocked even if it is on the listruntime.rs L582
  6. An empty or miss on the allowlist returns NotAllowedruntime.rs L598
  7. Only NotAllowed asks the decidernetwork_policy.rs L349
  8. Under Limited, anything but GET, HEAD, OPTIONS is blockedconfig.rs L311
  9. HTTPS must see the inner method; missing MITM blocks ithttp_proxy.rs L312
  10. Only if the header carries the fake is the matching real credential injectedmitm.rs L301
Hit Play to watch this request walk four gates, and see which step swaps the fake for the real value.
Domain firstDeny always wins. A local literal needs an exact allow. A name that resolves to the intranet cannot pass even if it is on the list.
The real key never leaves the brokerThe child env and the model context stay fake. The real token is swapped on only at the outbound hop.
The sandbox is the mouth of the funnelWhen Go skips the proxy, the policy function never runs. The OS only allows the proxy port; a direct connect dies at gate one.
Teaching sketch: the list, the fake’s shape, and the resolve result are course settings. Line numbers on the logic trail match openai/codex commit 4f39251a01.
Idea 1 · How a domain is judged
What problem it solves

You ask the model to file a GitHub issue. It writes curl; the env has a real token. A second later the host becomes evil.example, or it hits 169.254.169.254. Block only “hosts not on the list” and deny entries plus intranet addresses leak. If an empty list defaults to allow, forgetting the config is opening everything.

What the idea is

The judge order is written in stone. Deny first, then local or private, then the allowlist. An empty list and a miss both block. The decider can only retry a case the allowlist dropped — not the denylist, not a local segment.

Source:codex-rs/network-proxy/src/runtime.rs lines 549–552;codex-rs/network-proxy/src/network_policy.rs lines 346–378;codex-rs/network-proxy/README.md lines 161–162

*.example.com does not include the apex; **.example.com does. A name that resolves to a private IP is blocked even if it is written exactly on the list. A local literal must be written exactly as localhost or 127.0.0.1; a * glob does not count.

Source:codex-rs/network-proxy/src/policy.rs lines 321–331;codex-rs/network-proxy/src/runtime.rs lines 1028–1043

Resolve host A fail also blocks 1 deny A hit stops at once 403 denylist 2 Local or private The literal must be exact 403 allowlist 3 allowlist An empty list also blocks
Teaching diagram: the three-step order is fixed. Denied and NotAllowedLocal never ask the decider.

On a block the command process gets a 403; the header is x-proxy-error, the body is one human sentence. Outwardly not_allowed and not_allowed_local are both blocked-by-allowlist; only the body splits “not on the list” from “the sandbox blocked local”. That 403 becomes exec output fed back to the model; the sample loop does not stop.

Source:codex-rs/network-proxy/src/responses.rs lines 52–83

If the same pattern is both allow and deny, the effective value is the larger. Enum order is None < Allow < Deny.

Source:codex-rs/network-proxy/src/config.rs lines 19–27

Why it lasts

Default deny, deny always wins, the intranet needs an explicit door — that’s the generic SSRF-defense shape. Rewrite it in another language and the same questions still apply: what if the list is empty, who wins a conflict, does a name that resolves to the intranet count as allow?

Idea 2 · The real key only appears inside the proxy
What problem it solves

The token rides the command into the rollout. The next turn the model can still see it; the turn after that it may hit a log. Once a credential enters model context, every later redaction is a patch.

What the idea is

Before spawn, the credential broker swaps GH_TOKEN and OPENAI_API_KEY for fakes of the same length and prefix. If the model printenvs, it sees the fake. A fake in context is useless upstream.

Source:codex-rs/network-proxy/src/credential_broker.rs lines 92–119

On the way out, MITM filters by host. The request header must carry the fake before it is swapped for the real value. A hook’s strip/inject runs after the broker and can peel off the Authorization just injected. A user-edited env value is not restored. The mark is untrusted input. The broker only covers GitHub and OpenAI.

Source:codex-rs/network-proxy/src/mitm.rs lines 297–302

Model Cannot see the real token Child env Same-shape fake Proxy MITM Swap the real value by host Upstream Real token The real value only appears in the proxy process’s memory — not the rollout, not the model’s next-turn context
Teaching timeline: the fake walks every visible surface; the real value appears only on the outbound hop.
The child’s pocket holds a fake stub; the real ticket is locked behind the counter.
Why it lasts

If the outbound policy is wrong, the model can still change its mind from a 403. A credential in context is expensive to revoke. So the real value is taken before spawn, and the put-back happens in the proxy process’s memory. The fake keeps its shape so a client that checks format can still start.

Idea 3 · The sandbox presses traffic into the proxy
What problem it solves

Go’s net/http skips HTTP_PROXY for loopback. You think the traffic entered the proxy; it actually hit 127.0.0.1 directly. Locally there is an admin port, a docker socket. The policy file has a list; the request never reached that layer.

What the idea is

The sandbox only allows the proxy port. Restricted Seatbelt on macOS writes only localhost:{port}. Linux takes ProxyOnly. When allow_local_binding is false, NO_PROXY is written as an empty string, and a loopback literal must also take the proxy. Turning local binding on is admitting loopback no longer goes through the allowlist.

Source:codex-rs/sandboxing/src/seatbelt.rs lines 309–336;codex-rs/network-proxy/src/proxy.rs lines 450–460

Why it lasts

An application-layer proxy cannot stop a client that won’t take it. The next layer must be the OS or a firewall, with one mouth left. The three doors stacked together make a managed network. Any one door alone cannot cover a face another door dropped.

Side-by-side · The valve sits on a different layer

Grok: the valve is at the tool door; loopback is allowed by default

Grok’s web_fetch does a domain list and a post-resolve IP check inside the tool. An empty list blocks every URL — same direction as Codex’s allowlist-first. A path can also be written as a prefix, narrowed to one stretch of docs.

The SSRF check blocks RFC1918, link-local, CGNAT. Loopback is explicitly allowed; the comment says local development. curl in bash does not take this list. Codex puts the valve on every child-process exit, so it has to be tied to the sandbox.

Source checked on both sides · 2026-08-22 · Source:web_fetch/domain.rs lines 110–144;web_fetch/ssrf.rs lines 17–19

DSH: config stores only the variable name; spawn wipes the env by key

DSH makes the secret a reference. The settings file only carries the env-var name; the provider resolves on every operation. The config surface never sees the value.

The child has one more wipe. Keys named like KEY or TOKEN, and every DSH_*, are dropped. That stops a real value appearing in the config file. If the model can still printenv, unless spawn used this wipe, the env may still hold a real token.

Source checked on both sides · 2026-08-22 · Source: packages/credentials/credentials/src/index.ts lines 1–7; packages/subprocess/subprocess/src/index.ts lines 60–66 · DSH · Credentials resolved each time
Classroom Exercise
01

Does a star allow localhost?

allow_local_binding = false, and the allowlist is only *. Call the judge once on 127.0.0.1 — what do you expect? Call it again on the public IP 8.8.8.8 — what then?

Hint: a local literal refuses a glob. Allowlist compile explicitly allows a global *; denylist compile refuses it.

Takeaway:Outbound defaults to deny; deny always wins; the intranet needs an exact door. A real secret must not enter an env the model can see — swap a fake before spawn, swap again outbound. The proxy cannot stop a client that skips it; the next layer must be a sandbox that only allows the proxy port.