Skip to content

Codex ​

Codex is fullsend's third agent runtime. It runs OpenAI models only, through the same sandbox, egress policy and secretless credential path pi uses for GPT — the runner holds the credential, the sandbox never sees it. Turn it on for one repo, for one agent, or as a repos.yaml default.

bash
fullsend run triage --runtime codex --model openai/gpt-5.6-luna

Selecting it, and how it compares to the other runtimes, is in Agent runtimes. This page is what changes once you are on it.

Models ​

Codex takes an OpenAI model id — openai/<id> or the bare id — and nothing else. It serves the OpenAI Responses API, so there is no Claude, Gemini or Grok on codex.

The Claude aliases do not apply. opus, sonnet, haiku and fable name Anthropic models, so codex refuses them rather than picking a GPT model on your behalf:

codex takes OpenAI model ids only, and the Claude model aliases do not apply to it: "opus" is one
of them. To run this agent on codex, set FULLSEND_CODEX_MODEL=openai/<id> for the repo, or
model: openai/<id> on the agent's agents: entry or the harness

A model carrying another provider's prefix, and a run with no model named at all, fail the same way and name the same two fixes. Nothing is remapped: the per-repo models.aliases overrides are not consulted on codex either, so a Claude alias can never resolve to a GPT model behind your back.

That matters because the fleet harnesses ship model: opus. You do not need to edit them — there are two places to name a model for a repo on codex.

A default for every agent in the repo, set on the runner:

bash
FULLSEND_CODEX_MODEL=openai/gpt-5.6-luna

It is read only when codex is the runtime actually selected, and it sits below --model and FULLSEND_MODEL in the usual precedence — so it is a default, not an override. When it decides the model, metrics.json records override_source: FULLSEND_CODEX_MODEL and the plan block names it, the same way FULLSEND_PI_MODEL does on pi.

A model for one agent, on its agents: entry in .fullsend/config.yaml, which also outranks the harness:

yaml
agents:
  - name: triage
    runtime: codex
    model: openai/gpt-5.6-luna

Effort maps onto codex's own reasoning levels:

--effortRoot Codex model_reasoning_effort; native children retain their model defaults
lowlow
mediummedium
highhigh
xhighxhigh
maxmax

Codex has no equivalent of a fallback model chain: FULLSEND_FALLBACK_MODELS is ignored with a warning.

At a glance ​

CredentialsA runner-exchanged OpenAI WIF token in CI, or your OPENAI_API_KEY on the runner locally — never in the sandbox. Codex reads a placeholder from a runner-owned token file and re-reads it when the credential is refreshed (ADR 0092)
UnattendedApprovals off; codex's own sandbox off, because OpenShell is the boundary. A missing credential exits before the agent starts
Artifactsoutput.jsonl (the codex exec --json stream), transcripts/<agent>-<rollout>.jsonl, metrics.json with runtime: codex, plus codex-debug.log with --debug. Only uncompressed rollouts are extracted — codex compresses older sessions, so a .jsonl.zst is never the run's own transcript. The agent's final message is in the stream; --output-last-message also drops it in the runner-owned config directory inside the sandbox, which is a convenience when inspecting a kept sandbox rather than a downloaded artifact
Extra knobsFULLSEND_CODEX_MODEL (root model) and FULLSEND_CODEX_SUBAGENT_MODEL (child model default)
Sub-agentsNative named personas, generic and Explore children; see Native children for model and lifecycle requirements
Not supportedplugins:, fallback chains, non-OpenAI providers, resuming children

Codex does not report dollar cost. The terminal displays unavailable, and aggregate and per-model metrics carry cost_unavailable: true; a numeric zero in a legacy cost field is not evidence of a free run. Token counts include the parent and children, including failed children with recorded usage. Reasoning tokens are reported separately from non-reasoning output tokens.

Native children ​

Fullsend registers personas found in the selected harness skills as native Codex roles. The workflow selects the personas: review chooses its review dimensions and a separate challenger; retro chooses investigations relevant to the failure and dispatches each with agent_type: "default", including read-only and duplicate checks. The generic default and explore roles are available even when a harness supplies no personas. Use the companion instructions tracked in agents #1501; runtime support alone does not teach an older workflow how to dispatch native children.

For each child, model selection uses this precedence:

  1. Its explicit entry in agents[].subagents.
  2. The runner's FULLSEND_CODEX_SUBAGENT_MODEL.
  3. agents[].subagents.default.
  4. gpt-5.6-luna.
yaml
agents:
  - name: review
    runtime: codex
    model: openai/gpt-5.6-luna
    subagents:
      default: openai/gpt-5.6-luna
      security: openai/gpt-5.5

Use an actual registered persona name for an explicit override. Unknown names, Claude aliases, inherit, and non-OpenAI model prefixes are rejected. An unavailable explicit model fails rather than falling back. A null entry removes an inherited override. Claude model: and tools: frontmatter in a persona is reported but does not control the native child.

--effort sets root reasoning effort. Generated child roles select the model but do not set reasoning effort; native model defaults apply. In the local Luna runs, children used medium even when the root used low. Compare recorded per-thread effort when interpreting usage. The verified local Fullsend review/retro profile uses openai/gpt-5.6-luna with --effort medium. Earlier low-effort runs missed required reads or child closure; the runner rejects incomplete child evidence even when the final output passes its schema.

Children start with fresh context (fork_context: false), their role instructions and the task the parent explicitly supplies. The challenger therefore receives the review packet, without inheriting the parent's conversation. At most four child IDs can remain open, including completed children that have not been closed; children cannot spawn grandchildren. The parent must wait for a final result from every child and close every child before finishing, including the challenger. A wait that returns one completed child does not mean the other children finished. Resuming a child is blocked; start a fresh child instead.

These requirements apply even when optional security scanning is disabled. The native hierarchy inherits a Linux Landlock write restriction protecting the runner's configuration, roles and hook files. Bootstrap requires Python; launch requires Landlock ABI 3 or newer. An incompatible sandbox fails before inference rather than silently dropping this protection.

The verified lifecycle uses Codex's V1 collaboration API. The model's metadata selects that API, not just the CLI version. Local live workflow validation uses gpt-5.6-luna. In the inspected model catalogue, gpt-5.5 falls back to V1 and GPT-6 entries select V2; that inspection is not live workflow validation of those models. V2 has a different lifecycle and is not claimed supported for these workflows. Revalidate on a model or CLI change.

Each child has a separate transcript named with its native thread ID. Missing child usage, missing final results, unclosed children or transcript extraction failures fail the run even if the final output passes its JSON schema. Artifacts and known usage are retained for diagnosis.

Running it locally ​

Complete Running agents locally first — the CLI, OpenShell, credentials and the fleet clone are the same. Add OPENAI_API_KEY to an env file as that guide's OpenAI section describes, then add --runtime codex to any example on it:

bash
fullsend run triage \
  --fullsend-dir /tmp/fullsend-agents/ \
  --target-repo /tmp/target-repo/ \
  --env-file fullsend-gcp.env \
  --env-file fullsend-openai.env \
  --env-file fullsend-triage.env \
  --forge github \
  --runtime codex \
  --model openai/gpt-5.6-luna

The plan block confirms the selection — overridden values carry their source, harness defaults print bare — and metrics.json records the same (runtime, runtime_source, requested_model, override_source):

    Model: openai/gpt-5.6-luna
    Effort: high
    Runtime: codex (from --runtime flag)
...
runtime: selected "codex" from --runtime flag
...
→ Agent: gpt-5.6-luna (v0.157.0)
  ✓ Agent exited with code 0

The version on the Agent: line is the codex CLI the sandbox image carries, captured by Bootstrap's codex --version preflight — not the model's.

To keep an agent on codex (or off it) without passing flags every time, set runtime:/model: on its agents: entry in config.yaml — see per-agent settings.

What a local codex run needs, beyond the guide:

  • fullsend from the release that carries CODEX_VERSION — the first one cut after the codex runtime lands. The release download and the container image both work as-is.

  • A sandbox image that includes codex — ghcr.io/fullsend-ai/fullsend-sandbox built with CODEX_VERSION (0.157.0 today). A stale image fails Bootstrap's preflight before the agent starts, with codex preflight: `codex --version` exited 127; podman pull ghcr.io/fullsend-ai/fullsend-sandbox:latest fixes it.

  • A harness that declares the provider and a policy — providers: [openai] and policy: policies/base.yaml. The fleet's agents carry both from the first fullsend release after v0.43.0; up to v0.43.0 they carry only the policy. fullsend run materializes the credential only when the harness lists the provider, so on those releases add it on a child harness (a child's providers are appended to its base's). Runs that do not call OpenAI skip the provider. A custom harness also needs the policy, because the sandbox image's default policy leaves an uninspected route to api.openai.com, which the gateway refuses to carry the credential over.

  • Debugging — --debug='*' (the = is required); sandbox-side failures land in codex-debug.log inside the run directory, next to the transcripts, not in the runner's output.

  • Platforms — the runtime was brought up and smoke-tested on macOS Apple Silicon (podman machine, Homebrew openshell, arm64 image); the local guide's platform notes otherwise apply unchanged.

Behaviour differences worth knowing ​

  • No permission prompts. Codex, like pi, expects to run inside a container: the sandbox and its egress policy are what contain the agent, not a tool-approval dialog (ADR 0027).

  • Reads the repo's root AGENTS.md through $CODEX_HOME. Codex skips a project's own AGENTS.md while the project is untrusted, so the runner copies the repo's root AGENTS.md (or the org-level one fullsend injects when the repo has none) to $CODEX_HOME/AGENTS.md, which codex loads as instructions. Only the root file is read, cut at 32 KiB: nested AGENTS.md files and AGENTS.override.md are not, and a symlinked AGENTS.md is skipped. No CLAUDE.md bridge is injected.

  • The repository's own .codex/ config is never loaded. fullsend pins the cloned repo's project trust to untrusted, so .codex/config.toml and .codex/hooks.json (model, instructions, MCP servers, hooks) never apply. A target repo cannot change how the agent runs.

  • Tool lists are not native permissions. Codex works through a shell, apply_patch and native collaboration tools, so a harness tools: list has no native allowlist to map onto. A Bash(...) allowlist is recorded but not enforced on codex, and the run says so:

    Agent Bash allowlist (gh, curl, jq) is recorded but not enforced on codex
    (see docs/contributing/runtime-implementation.md)

    Entries with no codex equivalent are dropped, each with its own note — WebFetch, for instance, because codex reaches the web through the shell.

  • A blocked tool call reads as declined, not failed. A PreToolUse block reaches the model as Command blocked by PreToolUse hook: <reason>, so the agent understands it was refused rather than that the command broke — in a smoke run it summarised the block as "blocked by a safety hook" and moved on.

  • The security hooks can withhold a tool result, but cannot edit it. Codex does not let a hook replace the output of a built-in tool, so a result containing a secret, unsafe hidden Unicode or a canary is withheld instead of reaching the model unredacted. Context-only suppression and ANSI cleanup are skipped because those results are safe to pass unchanged; suppressed output runs through the full security chain again with suppression disabled first. OSC and unclassified rewrites are withheld, and so is a result the hooks run out of time to check (not enough of codex's hook budget is left to run posttool_chain.py). Your run artifacts are redacted either way — output.jsonl, the transcripts and codex-debug.log are all scrubbed before they are written.

  • Skills work as they do on Claude Code: the harness's skills, plus your repository's own .agents/skills (codex's documented location for repository skills), both scanned for injected content before the agent sees them. Codex 0.157.0 also picks up .codex/skills even though the project is untrusted; that is not documented upstream and may change, so keep repository skills in .agents/skills. Codex's bundled skills (skill-installer, imagegen and friends) are switched off, so an agent sees only yours. Harness skills are installed under their declared frontmatter name (directory name when absent); duplicate names from different directories fail bootstrap. Nested skills are retained when their parent directory is uploaded.

  • Shells are non-login shells. This preserves the runner's workspace helper and Go paths, including fullsend-check-output. Security scanners use a separately pinned base PATH.

  • Cost is unavailable — see At a glance.

Not yet exercised ​

Start on a disposable repo, with triage or prioritize before code or fix. Codex has been run end to end by hand — the fleet's own triage and review harnesses, on openai/gpt-5.6-luna, on macOS — but not yet through a full fleet lifecycle, and not yet on the CI credential path: the Workload Identity route needs an OpenAI organization mapped to the repositories, which does not exist yet, so local runs use OPENAI_API_KEY on the runner. Until that mapping exists codex also has no default behaviour-test coverage; its scenario is gated. What was run, and on which versions, is recorded in codex runtime internals.

Native review and retro require both this runtime implementation and the companion instructions. The cross-repository completion gate remains #6970 and agents #1500. Local synthetic workflows exercise analysis, child lifecycle, schema output, transcripts and usage; they do not establish live forge retrieval/posting, CI Workload Identity, Fedora coverage or the full fleet lifecycle. See the validation status before treating the combination as generally available.

Security hooks can add model turns when a blocked tool needs an alternative. Those turns are included in usage. Pre-tool denials and post-tool withholding were exercised in both parent and child contexts. A blocked tool alone does not immediately terminate a Codex session. At the end of each iteration, the runner verifies the downloaded audit and applies security.escalation.on_critical: the default halt fails on critical findings before validation, retries or the post-script, retaining output, transcripts and known usage. The host output scan also enforces this policy. review emits a manual-review warning without publishing a forge label. Tirith's structured blocking severity is preserved: a high-severity parser denial remains a blocked tool, while critical findings and unclassified adapter failures still halt. Read only explicitly named task inputs; environment dumps and recursive searches of runtime configuration can expose canaries and correctly fail the run. See harness security settings.

Troubleshooting ​

codex preflight: `codex --version` exited 127, or fullsend: codex not found on PATH (exit 127) once the run starts. The sandbox image predates the CODEX_VERSION pin, so there is no codex to run. Pull a current ghcr.io/fullsend-ai/fullsend-sandbox image.

Denied requests in the sandbox egress log at startup. Expected noise, not failures. Codex probes on the way up and the policy refuses what the run does not need:

DeniedWhy it appears
GET /v1/models on api.openai.comCodex refreshes its model catalog on a custom provider. The fullsend-openai profile allows only POST /v1/responses, so the probe is denied at L7, up to three times. The first allowed POST follows about 100 ms later.
chatgpt.com:443A sign-in/account probe the agent run has no use for; denied at L4.
api.github.com:443Denied at L4 from codex itself — the agent reaches GitHub through the gh CLI and its own provider, not from the model client.

None of these stop the run. What would is a denial on POST /v1/responses, which means the profile or the policy is wrong.

OPENAI_API_KEY in the sandbox is not a gateway placeholder. The harness does not declare the OpenAI provider, so no credential was attached to the sandbox. Each iteration stops before codex starts, and the run ends with validation failed after 2 iteration(s):

fullsend: OPENAI_API_KEY in the sandbox is not a gateway placeholder (openai provider not attached, or a real key reached the sandbox); refusing to run codex
→ Result: ERROR (incomplete)

Add - openai under providers: in the harness you run. The fleet harnesses declare it from the first fullsend release after v0.43.0; on an earlier release, or on a custom harness, add it yourself (on a child harness in a .fullsend repo).

provider auth command `...` ... — codex's own wording, one of exited with status N, failed to start, timed out after N ms, produced an empty token, or wrote non-UTF-8 data to stdout. Codex could not read the credential: the runner-owned token file is missing, or it does not hold a gateway placeholder. Check that the harness declares providers: [openai] and that OPENAI_API_KEY reached the runner, not the sandbox.

The run stops before the agent starts, naming api.openai.com. The effective sandbox policy admits api.openai.com:443 without protocol inspection, so the gateway refuses to carry the credential over it. Add policy: policies/base.yaml to the harness.

codex takes OpenAI model ids only .... The resolved model is a Claude alias (opus and friends), carries another provider's prefix, or is missing entirely. The message names both fixes: FULLSEND_CODEX_MODEL=openai/<id> for the repo, or model: openai/<id> on the agent's agents: entry or the harness. See Models.

A guard refused the run. The runner-written files under CODEX_HOME are checked before every launch, and a mismatch stops the run rather than continuing unprotected. Each message names what failed:

fullsend: codex config, hook adapter or auth script missing or modified; refusing to run
fullsend: the sandbox hook scripts are not the set fullsend installed (a file was changed,
replaced with another allowed file, or something was added); refusing to run
fullsend: codex config.toml or hooks.json is not the file fullsend wrote; refusing to run (a
rewritten config can trust the target repo, which loads its .codex/ layer and its hooks)

The first two exit 97 and mean the hook wiring cannot be trusted; the third exits 98 and is the credential-and-trust guard. Landlock setup or authority verification can also exit 78. The launched Codex hierarchy cannot write these files; check the sandbox kernel and any external sandbox mutation when diagnosing a failure.

--debug "..." fails with accepts 1 arg(s). --debug takes an optional value: write --debug='*' (with =).

The model answers, then the run fails anyway. A model your account cannot serve on the Responses API surfaces as several error reconnect events and a turn.failed carrying the 404 — not as a startup error. codex exec can still exit 0 there, so fullsend takes the verdict from the stream rather than the exit code, and the run fails as it should.

Run-scoped provider ... expired in place instead of deleted. The run finished and deleted its sandbox, but OpenShell still reported the provider attached to it, so fullsend expired the credential rather than leaving a live one behind and printed the openshell provider delete command to finish the job. The credential is dead either way; run that command to tidy up. Under --keep-sandbox the same expire-in-place is deliberate, because the sandbox you kept still references the provider.

The agent fails with nothing in the terminal. Codex has no debug flag of its own, so --debug='*' makes the runner export RUST_LOG and capture codex's stderr to codex-debug.log in the run directory, next to the transcripts. Kept sandboxes must be removed manually (openshell sandbox delete <name>).

The run used Claude instead of codex. The runtime falls back to claude when neither the config's runtime: (repo-wide or on the agent's agents: entry) nor --runtime/FULLSEND_RUNTIME selects codex; the plan block's Runtime: line and stderr's runtime: selected ... show which one ran and why.

See also ​