Skip to main content
The Agents API splits an agent in two. OpenAI hosts the model, the Codex harness, session state, and context compaction. A self-hosted environment moves the other half to you, so every shell command, file read, file write, and local MCP call executes on infrastructure you own. Point that half at boxd and each session gets a full Linux VM with its own kernel, root, a persistent disk, and an HTTPS domain. The machine boots in milliseconds, freezes between turns, and wakes with its filesystem exactly as the last turn left it.

An Agents API session. OpenAI runs the agent, the commands run in a boxd machine that did not exist when the session started.

How it fits together

The Codex CLI runs inside the machine as codex exec-server. It opens one outbound WebSocket to OpenAI, receives commands, and returns results. Nothing dials into your network, so the machine can keep every inbound port closed. Your app talks to OpenAI, which runs the model, session state and the Codex harness. Your boxd machine runs codex exec-server, which connects outbound over a WebSocket and runs shell commands, file reads and writes, patches, local MCP servers and skills. Three pieces:

Set up the OpenAI side

Two keys, both from the platform dashboard:
  • An application key for your app, with api.agents.read, api.agents.write, and api.responses.write. It creates sessions and sends input.
  • An environment key from the Agents tab, under Environments, then Keys, with every other permission set to None. It goes into the machine as CODEX_API_KEY, and connecting environments is all it can do.
Both must belong to the same organization, project, and user or service account.
The Environments tab of the OpenAI Agents dashboard, on Keys, listing one active environment key with its tracking id, masked secret, and creation date

Environment keys in the platform dashboard. Creating one is the one step that lives here, and the dashboard notes that self-hosting is the only thing they are for.

Keep the application key off any machine that runs the agent’s commands. The environment key is enough for the executor, and it grants nothing beyond connecting.

Quickstart, one machine

Create a machine and give it a /workspace. The boxd image already ships the Codex CLI and ripgrep:
Create a session from anywhere. OpenAI answers with the environment id and the URL the executor connects to:
Start the executor in the machine with the environment key passed in on the command:
Now send the first turn:
The stream reports agent.session.environment.connected the moment the executor registers, and the turn follows. The file appears on the machine.
Set --auto-hibernate-timeout=0 on any machine running an executor, and --auto-suspend-timeout=0 if your org turns auto-suspend on by default. The idle timers watch inbound traffic, and the executor’s WebSocket is outbound, so a busy executor still looks idle. See Suspend, resume, and hibernate.

One machine per session

A single machine means every session shares one filesystem. Giving each session a machine of its own buys three things. Sessions cannot see each other’s files. A session that wrecks its machine wrecks only its own. And each machine sleeps on its own schedule. The shape is an orchestrator that answers OpenAI’s connection requests with a machine.

Build the image once

Every session machine boots from a snapshot, so the toolchain is already in place:
A machine created from this snapshot is ready in milliseconds, memory and all. OpenAI’s own setup installs @openai/codex@alpha with npm. The build on the boxd image registers with the current API, and the builder is the place to pin a different one.

Run the orchestrator

Three webhooks drive it. agent.session.action_required with an environment_connection action means a session needs its machine, so the orchestrator boots or wakes one and starts the executor. agent.session.idle starts a grace period, after which it stops the executor and pauses the machine. agent.session.failed deletes the machine. Put it on its own boxd machine, where the SDK authenticates automatically with no key to manage.
Create the orchestrator machine first, so the URL you register resolves:
Register https://codex-orchestrator.boxd.sh/webhook as a webhook endpoint in your project’s settings, subscribed to agent.session.action_required, agent.session.idle, and agent.session.failed. Put the whsec_ secret it shows next to the two keys, in a .env beside your orchestrator.py or orchestrator.ts:
The application key here only retrieves sessions, so api.agents.read is enough for it. Then deploy. The machine already has an HTTPS domain, so there is no TLS to arrange:
A signed delivery gets a 204 in ~/orchestrator.log. Anything unsigned gets a 400. Your application now needs nothing from boxd. It creates a session and sends input, and the machine appears:

Pause between turns

Thirty seconds after a session goes idle, the orchestrator checks that nothing new has arrived, stops the executor, and pauses the machine. That takes it to standby, where it holds its memory and processes and costs near nothing. Stopping the executor first is the whole trick. OpenAI sees the environment go offline, so the next input produces a connection request instead of a tool call into a frozen socket. The orchestrator wakes the machine, starts a new executor, and the turn proceeds. The agent finds /workspace as it left it, dependencies installed and caches warm. The executor itself holds nothing worth keeping, since the session lives at OpenAI. The machine lives until the session fails, or until you remove it. Deleting a session fires no webhook, so delete the session and its machine together from your app, or sweep the agent- machines whose session no longer exists.

Your app drives both

Without a webhook, your app can do the orchestrator’s job itself, because it knows when it sends input. Wake the machine and start the executor before the turn, stop the executor and pause after it. ensure_machine and start_executor are the ones from the orchestrator above:
Same machines, same snapshot, same pause between turns, with one process fewer to run. Use it when one application owns every session. Use the webhook when sessions are created from several places, or when the process that sends input should know nothing about sandboxes.

Credentials the agent can use but never read

The environment key is the one credential the machine holds, and connecting is all it can do. Everything else the agent needs, an API key for a service it calls or a token for a private registry, can stay out of the machine entirely. Bind a secret to the hosts it may be sent to, and the machine only ever holds a placeholder:
The real value is substituted into requests to api.stripe.com on the way out. Inside the machine, env, shell history, and the agent’s own transcript contain nothing but an opaque bxds_… string. A prompt injection that talks the agent into printing every secret it can find gets placeholders. Set it once at the organization level and every session machine carries it. An isolated machine receives account-level variables through exec sessions, and exec is exactly how the orchestrator starts the executor. Pair it with a per-machine egress allowlist so the machine can only reach the hosts you name. A machine restored from a snapshot starts unrestricted, so set the list in ensure_machine right after create. Keep api.openai.com and codex-cloud-environments.chatgpt.com on it, the two hosts the executor needs:
Both controls are enforced outside the machine, so code inside cannot lift them. See Env vars & secrets and Egress control.

Files and repositories

Self-hosted sessions take no environment.files at creation and publish nothing through OpenAI’s Artifacts API, since OpenAI has no container to mount them into. Two boxd answers, depending on whether the data is shared or per-session: Shared across every session. Bake it into the snapshot. Clone the repo, install the dependencies, warm the caches, put skills where capability_directories will find them, then save. Every session starts with it and pays nothing at boot. Per session. Copy it in from the orchestrator before starting the executor, using the session’s own metadata to decide what. Your app sets the metadata when it creates the session:
The orchestrator already retrieves the session in connect, so the metadata is right there:
Session outputs work the same way in reverse. The agent writes to /workspace, and the orchestrator reads the file out with machines.files.download once the turn ends.

Parallel exploration

A fork copies a running machine’s disk, memory, and processes in under 200ms. Fork a session machine several times mid-task and each copy continues from the same live state, so an agent can try several approaches at once and keep the one that works:
Expose it to the agent as a small CLI it can call through the shell, or drive it from the orchestrator. Agent swarm intelligence covers the fan-out and merge pattern in full.

What you own

Commands and their output still travel to OpenAI, because the model has to see them. Everything else stays on your machine.

FAQ

No. The executor connects outbound over a WebSocket. Only the orchestrator needs an inbound URL, and only if you choose the webhook path. Every machine already has one at name.boxd.sh.
The command in flight fails, and the turn completes with that failure visible in the agent’s answer. OpenAI does not request a connection mid-turn. The next input does, and the orchestrator boots a fresh machine from the snapshot under the same name, since the old one is gone. Files written since the snapshot went with it.
Yes, that is the quickstart. Each session needs its own executor, and two executors run side by side in one machine, each with its own environment id. It suits a trusted single-tenant workload. Give each session its own machine when the sessions should not see each other’s files.
OpenAI refuses deletion while a session has a required action. Connect an executor for a few seconds, the action clears and the session goes idle, then delete it.
Whatever you pass in agent. The environment has nothing to do with model choice, so a self-hosted sandbox costs the same per token as a hosted one.