Skip to the content.

fak + OpenAI Codex

fak puts a structural policy gate in front of Codex tool use.

TL;DR: Use fak serve --stdio as an MCP server for current Codex CLI and IDE sessions.

go run ./cmd/fak preflight --policy examples/customer-support-readonly-policy.json --tool refund_payment --args "{}"

Fastest path

Codex is OpenAI’s coding agent for software development. Its current surfaces include the Codex CLI, IDE extension, Codex app, and cloud tasks. This guide keeps those surfaces separate from the generic OpenAI-compatible API path.

There are two useful fak entry points:

If you run… Use this fak path Why
Current Codex CLI or IDE extension fak serve --stdio as an MCP server Codex supports MCP, and fak exposes verdict tools without changing Codex’s model wire.
Codex CLI with an OpenAI API key and you want fak in front of the model wire fak codex -- <codex args...> One command starts fak guard, launches Codex, and injects per-run Codex -c model_provider=fak overrides for the Responses wire.
OpenAI SDKs, OpenAI Agents SDK, LangChain, LlamaIndex, or any Chat Completions client fak serve as an OpenAI-compatible gateway The client already calls /v1/chat/completions, so you repoint its base URL to fak. Endpoint-by-endpoint compatibility and current limits: openai.md.

Honest wire boundary: current Codex model-provider docs are Responses-oriented. fak can proxy to an OpenAI Responses upstream with --provider openai-responses. The public gateway clients hit today are /v1/chat/completions, /v1/responses, /v1/messages, /mcp, and /v1/fak/*. fak now exposes a client-facing /v1/responses inbound route (#925): a Responses-API agent repoints its OpenAI base URL at fak and every proposed tool call crosses the kernel’s capability floor, the same as the chat wire. It is buffered — a stream:true request is refused with a 400, so a client that needs SSE should use MCP. For current Codex CLI/IDE sessions either path works; for OpenAI-compatible SDKs and Chat Completions agents, use the base-URL proxy path below.

Why this matters to Codex

Codex reads AGENTS.md before it works in this repo. The repo-level rules already tell it the build, test, commit, and guardrail contract. fak adds a second layer: the kernel can adjudicate proposed tool calls and tool results with a default-deny floor that a prompt cannot talk around.

Use the right path for the job:

Do not treat “Codex can reach /v1/responses” as the whole integration contract. The guarded model-wire path also has to match Codex’s host-tool dialect: exact tool names such as update_plan, namespace-qualified spellings such as functions.update_plan, dangerous argument fields, and Codex’s behavior after a denied tool. The acceptance gate for that surface is the harness integration acceptance checklist; the failure mode it prevents is recorded in the 2026-07-03 tool-dialect postmortem.

60-second proof before wiring Codex

From the repository root:

go run ./cmd/fak preflight --policy examples/customer-support-readonly-policy.json --tool refund_payment --args "{}"
go run ./cmd/fak preflight --policy examples/customer-support-readonly-policy.json --tool search_kb --args "{}"
go run ./cmd/fak agent --offline

Expected shape:

That proves the capability floor is structural, not a model judgment.

Codex keeps its MCP child executable open for the life of a session. Pointing it at the mutable worktree binary can therefore block upgrades on Windows and can silently separate the source revision from the process Codex is actually running. Install a content-addressed, self-contained copy instead:

fak codex mcp install --policy C:\absolute\path\to\policy.json
fak codex mcp status

install copies the binary and policy outside the worktree, records the module revision and SHA-256 checksum, atomically patches only [mcp_servers.fak], backs up the previous entry, and performs a bounded MCP initialize handshake. Each upgrade gets a new filename, so an old Codex session may keep the previous binary open while new sessions select the new copy. Restart or reload Codex after install/upgrade; existing sessions do not reread config.toml.

Useful recovery operations:

fak codex mcp install --rollback   # restore the prior known-good entry
fak codex mcp uninstall            # remove only installer-owned config/artifacts

status reports checksum/config drift, missing or wrong-architecture artifacts, stale source revisions, and initialize-probe failures. It never prints credentials. Use --config to test against an isolated Codex config fixture; all policy and command paths written to TOML are absolute.

Path 1: Current Codex CLI or IDE extension via MCP

Build the binary:

go build -o fak ./cmd/fak

One-command guarded Codex launcher

For Codex CLI sessions where you have an OPENAI_API_KEY available and want fak to mediate Codex’s model wire directly, use the launcher:

./fak codex --dry-run --split off -- exec --json "List active MCP servers only."
./fak codex -- exec --json "Summarize AGENTS.md."

The dry-run should print a command shaped like:

fak guard --split off ... -- codex --dangerously-bypass-approvals-and-sandbox exec --json ...

Live continuation-hook contract witness

Codex CLI 0.144.1 was exercised end to end with the reviewed repo-level .codex/hooks.json. The CLI discovered the manifest, invoked UserPromptSubmit with session_id, and honored the {"decision":"block","reason":...} response before model execution (zero input/output tokens). The privacy-preserving captured payload and normalized event evidence are committed at experiments/agent-live/codex-continuation-hook-live-witness-2026-07-11.json.

Continuation guard and intentional direct sessions

The reviewed Codex UserPromptSubmit continuation hook runs fak sessions codex-loop-hook before a continued turn. If the session is using a direct model provider, the hook blocks the turn because fak cannot enforce the guard on that model call; relaunch with fak codex (preferred) or fak guard -- codex.

For a deliberately direct-provider continuation, pass --allow-direct when invoking the hook, or set FAK_ALLOW_DIRECT_CODEX_CONTINUE=1 in the hook environment. In PowerShell use $env:FAK_ALLOW_DIRECT_CODEX_CONTINUE=1; on POSIX shells prefix the Codex command with FAK_ALLOW_DIRECT_CODEX_CONTINUE=1. The hook intentionally fails open on unreadable/missing session evidence or other internal diagnostic errors, and writes a diagnostic to stderr rather than blocking unrelated Codex work.

After the child exits, fak codex also tries to find the newest Codex session JSONL touched during the run and writes privacy-preserving vCache artifacts: sanitized token counters plus a fak.vcache.score.v1 score under the user config dir (fak/codex-vcache/), and the default observed-window snapshot consumed by fak vcache score. Use --vcache-out-dir DIR to relocate those artifacts, --vcache-snapshot=false to skip the default snapshot update, or --vcache-artifacts=false to disable the post-run extraction.

At runtime fak guard rewrites the Codex child argv to include:

-c model_provider=fak
-c model_providers.fak.base_url="http://127.0.0.1:<port>/v1"
-c model_providers.fak.wire_api="responses"
-c model_providers.fak.env_key="OPENAI_API_KEY"

That path is API-key billing today. A codex login ChatGPT subscription remains best used with the MCP path below until subscription auth is wired through the guarded Responses proxy.

Optional self-check for the MCP server:

python examples/mcp/verify.py

Add fak to Codex as a local MCP server:

codex mcp add fak -- ./fak serve --stdio --policy examples/dev-agent-policy.json

Then verify Codex can see it:

codex exec --json "List the active MCP servers, then summarize AGENTS.md."

In the interactive Codex CLI, /mcp should show the fak server. In the IDE extension, Codex uses the same config.toml MCP configuration as the CLI.

What Codex gets from this path:

MCP tool surface What it proves
fak_adjudicate Ask the kernel for a verdict before running a call.
fak_syscall Let the kernel adjudicate and execute a registered call.
fak_admit Screen a tool result before it re-enters model context.
fak_context_change Read the “what changed” feed when a shared state surface is present.

Use this path when you are running Codex itself. It preserves Codex’s current model wire and adds fak as an explicit, inspectable tool boundary.

Long-context reset budgets

There are two different questions:

For an OpenAI-compatible client that can repoint its base URL, seed a stable served session and context budget:

fak serve \
  --addr 127.0.0.1:8080 \
  --provider openai \
  --base-url "$UPSTREAM_OPENAI_COMPAT_BASE" \
  --session-id codex \
  --context-budget-tokens 150000 \
  --reset-on-budget \
  --policy examples/dev-agent-policy.json

Then point the client at http://127.0.0.1:8080/v1. With --reset-on-budget, when the normalized prompt/context usage exhausts the budget the gateway mints a continuation id, distills the refused transcript into a carryover seed, re-arms the continuation trace with a fresh 150k budget, and retries the live request under that new trace.

Without --reset-on-budget, the next request returns 409 with the usual error envelope plus:

For fak guard, use the restart supervisor when the wrapped client benefits from a real child-process boundary:

fak guard --provider openai --context-budget-tokens 150000 --restart-on-budget -- <openai-compatible-agent>

On budget exhaustion, guard distills the served transcript into a carryover seed, re-arms the continuation trace, writes a seed JSON file, advances the default trace for callers that omit X-Trace-Id, stops the child, and relaunches it with:

Use --restart-limit N to cap relaunches and --restart-seed-dir DIR to choose where the seed handoff files are written. The older --reset-on-budget mode remains available for clients that want the gateway to retry in-place without killing the child process. A generic child that ignores FAK_RESET_SEED_FILE still restarts under the fresh trace, but will not automatically rehydrate its local transcript.

Current Codex CLI/IDE sessions should still use MCP first. If that Codex surface does not honor an injected OpenAI-compatible base URL, fak can adjudicate tools but cannot independently observe provider context usage; use fak_session_reset only when the Codex side or a wrapper can report the context-token count it wants fak to debit.

Cooperative MCP reset call shape:

{
  "name": "fak_session_reset",
  "arguments": {
    "trace_id": "codex",
    "context_tokens": 150001,
    "messages": [
      {"role": "system", "content": "You are working in C:\\work\\fak."},
      {"role": "user", "content": "Continue the reset implementation."}
    ]
  }
}

The response has reset: true, from_trace_id, to_trace_id, a reset_directive.action of restart_fresh_session, and seed_messages when the reset was accepted. A reset: false result is a normal refusal value: the session was not budget-drained, or the gateway was not started with --reset-on-budget.

Prove Codex actually used fak

The MCP server being configured is not enough evidence on its own. Prove a Codex session called the fak server and keep the proof privacy-preserving:

codex mcp get fak
python tools\codex_dogfood_witness.py --thread-id $env:CODEX_THREAD_ID --run-codex-exec

The witness writes experiments/agent-live/codex-dogfood-<thread>.json plus a sanitized usage JSONL. It copies token counters, fak verdicts, MCP call metadata, and DOS hook counts; it does not copy prompts, tool arguments, tool outputs, diffs, or model text.

When Codex was launched outside fak codex, turn the same Codex token counters into the default vCache value surface by extracting a privacy-preserving cache snapshot after the run:

.\fak vcache codex-session-extract `
  --thread-id $env:CODEX_THREAD_ID `
  --out experiments\agent-live\vcache-codex-$env:CODEX_THREAD_ID.jsonl `
  --snapshot-out default `
  --score-out experiments\agent-live\vcache-codex-score-$env:CODEX_THREAD_ID.json
.\fak vcache score --json

The extractor writes only token counters and a codex prefix-family label. With --snapshot-out default, fak vcache score reads that observed Codex window by default instead of falling back to the synthetic forecast. --score-out writes the immediate fak.vcache.score.v1 verdict for the same window; provider rebate remains OBSERVED telemetry and is still not treated as trust.

A good run has this shape:

checks.dos_session_audit.status may still be WARN. That is useful dogfood evidence, not a failed proof: it means DOS saw host calls whose file-tree footprint was opaque while a lane lease was live. If checks.codex_hook_fast_path.status is already PASS, the warning is not caused by Python hook-manifest wiring; prefer path-visible tool calls or narrower shell commands, then rerun the witness and compare summary.dos.session_advisory_by_tool and summary.dos.unknown_tree_warning_rate. For the single-session witness, summary.codex_actionability splits actionable risk from residual debt: delegates, stop failures, out-of-tree writes, and malformed shell arguments are actionable; HOST_SHELL_OPACITY and UNKNOWN_TREE_WARNINGS remain privacy-preserving upstream-footprint debt when the post-repair delegate count is zero. This actionability block is scoped to the current Codex thread, so it can stay clean while a later multi-session transfer audit warns about another recent session.

Gate local Codex commands through fak

When Codex is about to run a local validation or build command, wrap it with the same policy floor instead of treating the shell as trusted:

python tools\codex_fak_gate.py `
  --tool run_tests `
  --redact-command `
  --command-label dogfood-witness-test `
  --out experiments\agent-live\codex-fak-gate-dogfood-witness-test-$env:CODEX_THREAD_ID.json `
  -- python tools\codex_dogfood_witness_test.py
python tools\codex_fak_gate.py `
  --tool run_tests `
  --redact-command `
  --command-label dos-recent-audit-test `
  --out experiments\agent-live\codex-fak-gate-dos-recent-audit-test-$env:CODEX_THREAD_ID.json `
  -- python tools\codex_dos_recent_audit_test.py
python tools\codex_fak_gate.py --tool go_test -- go test ./cmd/fak -run "TestRunVCache|TestReadVCacheTelemetry"

The wrapper calls fak preflight first. If the named operation is denied, the command does not run:

python tools\codex_fak_gate.py `
  --tool git_add `
  --expect-deny `
  --expect-reason DEFAULT_DENY `
  --redact-command `
  --command-label git-add-deny `
  --json `
  --dry-run `
  --out experiments\agent-live\codex-fak-gate-git-add-deny-$env:CODEX_THREAD_ID.json
python tools\codex_fak_gate.py `
  --tool git_commit `
  --expect-deny `
  --expect-reason DEFAULT_DENY `
  --redact-command `
  --command-label git-commit-deny `
  --json `
  --dry-run `
  --out experiments\agent-live\codex-fak-gate-git-commit-deny-$env:CODEX_THREAD_ID.json
python tools\codex_fak_gate.py `
  --tool git_push `
  --expect-deny `
  --expect-reason POLICY_BLOCK `
  --redact-command `
  --command-label git-push-deny `
  --json `
  --dry-run `
  --out experiments\agent-live\codex-fak-gate-git-push-deny-$env:CODEX_THREAD_ID.json

Use this for Codex’s own operating loop: run_tests before Python test commands, go_test before Go test commands, default-denied names such as git_add and git_commit before local history mutation, and deny-listed names such as git_push before any publish path. JSON reports record the verdict, command identity, and exit code; command stdout/stderr are dropped unless --include-command-output is set.

Fold the gate reports into the dogfood witness when you want one report to prove both Codex MCP usage and local command admission. Repeat --gate-report for every validation command the proof depends on:

python tools\codex_dogfood_witness.py `
  --thread-id $env:CODEX_THREAD_ID `
  --run-codex-exec `
  --gate-report experiments\agent-live\codex-fak-gate-dogfood-witness-test-$env:CODEX_THREAD_ID.json `
  --gate-report experiments\agent-live\codex-fak-gate-dos-recent-audit-test-$env:CODEX_THREAD_ID.json `
  --gate-report experiments\agent-live\codex-fak-gate-git-add-deny-$env:CODEX_THREAD_ID.json `
  --gate-report experiments\agent-live\codex-fak-gate-git-commit-deny-$env:CODEX_THREAD_ID.json `
  --gate-report experiments\agent-live\codex-fak-gate-git-push-deny-$env:CODEX_THREAD_ID.json

That adds checks.local_fak_gate_reports.status: PASS and summary.local_fak_gate.status: PASS to the witness, with summary.local_fak_gate.total showing how many checks passed. DENIED_EXPECTED reports count as passing local-gate evidence and also increment summary.local_fak_gate.expected_denied. Use --redact-command --command-label <stable-name> for durable reports: the command still runs, but the report keeps only a label, executable name, argc, and SHA-256 digest instead of the raw argv.

Post-run DOS audit for Codex sessions

After a Codex run, fold the DOS hook stream before treating the run as clean:

python tools\codex_dos_recent_audit.py `
  --repo-root . `
  --codex-home $env:USERPROFILE\.codex `
  --limit 10 `
  --since-days 7 `
  --check-latest `
  --out experiments\agent-live\codex-dos-recent-audit.json

For a local transfer gate:

python tools\codex_dos_recent_audit.py `
  --repo-root . `
  --codex-home $env:USERPROFILE\.codex `
  --limit 10 `
  --since-days 7 `
  --fail-on-warn `
  --max-unknown-tree-rate 0.02 `
  --max-delegates 0 `
  --gate-report experiments\agent-live\codex-fak-gate-git-add-deny-$env:CODEX_THREAD_ID.json `
  --gate-report experiments\agent-live\codex-fak-gate-git-commit-deny-$env:CODEX_THREAD_ID.json `
  --gate-report experiments\agent-live\codex-fak-gate-git-push-deny-$env:CODEX_THREAD_ID.json

The report copies only session filenames, thread IDs, timestamps, tool names, counts, and latencies. It flags tree_known=false admission warnings, native-hook delegates, stop blocks, and whether the cached Codex hook manifest uses the native DOS launcher or the Python CLI hook path. A Bash-dominated report means the hook could not prove precise file-tree footprints for the run; use narrower shell calls where the host can derive a tree, prefer MCP/fak verdict surfaces for checkable calls, and file upstream footprint-derivation debt when the rate stays above the transfer-playbook threshold. using_latest: true only proves package freshness; codex_hook_fast_path.status: PASS proves the Codex hook manifest is actually wired to the fast path.

If codex_hook_fast_path.status is WARN, inspect the manifest repair first:

python tools\codex_dos_hook_doctor.py --codex-home $env:USERPROFILE\.codex

The dry-run prints projected hook modes after apply. A projection with native Codex hooks and zero Python Codex hooks proves the repair would clear the fast-path warning before you write the cache.

Then apply it explicitly:

python tools\codex_dos_hook_doctor.py --codex-home $env:USERPROFILE\.codex --apply

The doctor keeps Python as the delegate fallback; it only changes the first path Codex hooks try.

After the repair, read post_repair_observations separately from the whole recent window. A whole-window delegate count can include old Python-hook history; a post-repair delegate count of 0 proves the fast-path issue is gone. If the report still shows shell_no_write_target_detected under post_repair_command_shapes, the remaining warning is shell opacity from read/inspect calls. Prefer host-visible read/search tools when Codex exposes them; otherwise keep the WARN as upstream footprint-derivation debt rather than treating it as a write-safety finding. If the family lens shows git_write, the actionable gate should fail: commit, add, push, and similar operations are opaque mutations and need an explicit operator gate. Supplying the three expected-deny Git gate reports proves a structured gate timestamp. The audit then also checks the post-gate Codex window; if another thread runs opaque git_write after that timestamp, the transfer gate remains WARN even though the single-thread witness can still be clean.

For automation that should fail only on post-repair actionable risk, use --fail-on-actionable-warn --max-delegates 0. Keep --fail-on-warn for the stricter transfer gate that still fails on residual shell-opacity debt.

The recent-audit command is intentionally multi-session: it folds the DOS-matched Codex threads included in sessions_audited, so a git_write family from a peer or older audited stream can make the transfer gate fail even when the single-thread dogfood witness is clean. Use mutating_shell_sessions to identify the sanitized thread/file bucket, then keep that failing report as transfer-gate evidence; do not fold it into checks.local_fak_gate_reports unless the witness is meant to fail closed too.

After the structured Git deny probes exist, pass them back into the recent audit:

python tools\codex_dos_recent_audit.py `
  --repo-root . `
  --codex-home $env:USERPROFILE\.codex `
  --limit 10 `
  --since-days 7 `
  --gate-report experiments\agent-live\codex-fak-gate-git-add.json `
  --gate-report experiments\agent-live\codex-fak-gate-git-commit.json `
  --gate-report experiments\agent-live\codex-fak-gate-git-push.json `
  --fail-on-actionable-warn `
  --max-delegates 0

That gate passes only when the expected-deny Git probes are valid and no audited Codex session contains a new git_write family after the latest probe timestamp.

To file or track that residual without leaking session content, add --out-debt experiments\agent-live\codex-dos-host-opacity-debt.md. The packet copies counts and shell shape/family categories only, including any mutating family counts.

Path 2: OpenAI-compatible clients through fak serve

Start fak in front of an OpenAI-compatible upstream:

go build -o fak ./cmd/fak
./fak serve \
  --addr 127.0.0.1:8080 \
  --provider openai \
  --base-url http://localhost:11434/v1 \
  --model qwen2.5-coder \
  --policy examples/dev-agent-policy.json

Then repoint an OpenAI-compatible client:

export OPENAI_BASE_URL="http://127.0.0.1:8080/v1"
export OPENAI_API_KEY="fak-local"

For Python SDK clients:

from openai import OpenAI

client = OpenAI(
    base_url="http://127.0.0.1:8080/v1",
    api_key="fak-local",
)

response = client.chat.completions.create(
    model="qwen2.5-coder",
    messages=[{"role": "user", "content": "List the Go packages in this repo."}],
    tools=[{
        "type": "function",
        "function": {
            "name": "Bash",
            "description": "Run a shell command",
            "parameters": {
                "type": "object",
                "properties": {"command": {"type": "string"}},
                "required": ["command"],
            },
        },
    }],
)

For TypeScript SDK clients:

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "http://127.0.0.1:8080/v1",
  apiKey: "fak-local",
});

Use this path when a framework already lets you set an OpenAI-compatible base URL. Good fits include:

What the kernel blocks for coding workflows

examples/dev-agent-policy.json is the coding-agent floor. It allows ordinary read/search/list flows plus build and test commands. It blocks publish and self-modification surfaces.

Attempt Kernel result
Read/search/list calls Allowed when the tool is on the allow-list or prefix allow-list.
git_diff, git_log, git_status, go_build, go_test, run_tests Allowed by the dev-agent policy.
git_add, git_commit Denied by the default-deny floor unless routed through a narrower release/ship gate.
git_push, git_merge, git_tag Denied with POLICY_BLOCK.
Writes to .git/, internal/kernel/, internal/policy/, VERSION, or dos.toml Denied by the self-modify floor.
Secret-shaped fields such as api_key, token, or authorization Redacted or quarantined by result-side guards.

Check one call without launching a model:

./fak preflight --tool git_push --args "{}" --policy examples/dev-agent-policy.json

Using a Responses upstream

If your upstream model provider is the OpenAI Responses API, fak can still be useful as the gateway’s upstream client:

./fak serve \
  --addr 127.0.0.1:8080 \
  --provider openai-responses \
  --base-url https://api.openai.com/v1 \
  --api-key-env OPENAI_API_KEY \
  --policy examples/dev-agent-policy.json

Clients still call fak’s supported inbound routes. That means:

Troubleshooting

Symptom Fix
Codex cannot see the MCP server Run codex mcp --help, re-add the server, then check /mcp in the Codex TUI.
codex exec --json has no fak events The MCP server is not enabled for that Codex run, or the task did not call fak.
OpenAI SDK gets 404 OpenAI-compatible clients need the /v1 suffix: http://127.0.0.1:8080/v1.
Anthropic SDK gets 404 Anthropic clients need the origin without /v1: http://127.0.0.1:8080.
Everything is denied Load a policy with --policy; with no policy the floor fails closed.
You tried to point default Codex model traffic at fak Use MCP instead, or use a client/framework path that explicitly speaks Chat Completions to fak.

Source alignment

This page was checked against the current OpenAI Codex manual on 2026-06-25:

fak-side references:

Diagnose an MCP startup before Codex

When Codex reports only connection closed: initialize response, run the same configured stdio child through the read-only staged probe:

fak doctor mcp --server fak
fak doctor mcp --server fak --json

The command reads [mcp_servers.fak] from $CODEX_HOME/config.toml (or ~/.codex/config.toml), never renders its env table, resolves the executable and version, checks the policy path, spawns the child, writes a real MCP initialize, and classifies the first stdout frame. The stable fak-doctor-mcp/1 JSON report names stages such as CONFIG_INVALID, EXECUTABLE_MISSING, POLICY_UNREADABLE, POLICY_MALFORMED, SPAWN_FAILED, INITIALIZE_TIMEOUT, EARLY_EXIT, and STDOUT_CONTAMINATION, each with a checkable recovery action. It only probes and terminates the child; it never quarantines or rewrites production state.

To diagnose an exact command instead of Codex config, put its arguments after the flags:

fak doctor mcp --command C:\path\to\fak.exe serve --stdio --policy C:\path\policy.json