Gustav Hartz (@GustavHartz) on X

X (formerly Twitter) ยท

13 min read Original article โ†—

TL;DR OpenAI's Codex Security is just JS calling Codex in a loop. Codex is given a stack of skill files, plus an MCP server that starts more Codex sessions. The loop that decides how many to start and when to stop is JavaScript, running against a file list generated by Python. Every discovery worker gets the same prompt and the same whole file list. Workers that do the actual analysis can't write a file: sandboxes are read-only, and the only write path is a schema-checked tool. Not one prompt in the repo contains security advice.

OpenAI recently released @openai/codex-security a couple of days ago, and it already has almost 9k stars. I wanted to try it: installed the CLI, connected my Codex subscription, hit "no Trusted Access".

So instead I decided to find out how it works so I can port it so I can try it out with Kimi and DeepSeek

This post is about how it works, but it might go out of date quickly since OpenAI is actively developing it and e.g. rewrote the discovery half on August 4, in #267 and #270, while I was writing this post...

The structure

It's JavaScript that loops and drives Codex with pretty generic prompts. There are a couple of different modes to run the analysis, but a deep scan runs four levels deep. You can see the levels below in the graph, but it's basically:

  1. your CLI
  2. a parent agent - that only launches a coordinator
  3. a coordinator - which is a js loop
  4. a pool of workers (codex sessions) that can each spawn nested subagents.

Work flows down that chain, and results climb back up it: nested threads report to their worker, workers to a merge agent, and the merged set lands back with the parent, which turns it into the report. The rest of this post walks the chain top to bottom, then follows the results back up.

The MCP manifest sets tool_timeout_sec: 86400. The parent basically spends most of that time blocked on one tool call.

Every "seat" runs the same base Codex agent. A seat is defined by its prompt, its tool list, and its working directory.

Codex security consists of 13 skill files, 22 reference docs and about 19k lines of Python. Three prompt templates run the whole system, two bundled with the MCP server and one assembled in the SDK. Scan state lives in a SQLite file created at runtime.

The repo calls two different things "coordinator": the code layer that runs the loop, and the parent Codex agent, which worker prompts address as "the top-level coordinator". I use coordinator for the code and parent agent for the model.

The file list

The model does not pick the files to analyze. Before any worker starts, the coordinator shells out to generate_in_scope_files.py, which runs the command below, sorts the output, and writes it once. That list is the inventory for the whole scan. Everything ripgrep can see is on it: dotfiles, tests, docs, CI workflows, even gitignored files. Nothing but .git is excluded, and nothing is ranked.

The skills

The skills are where the actual security knowledge lives. Every session is the same base Codex agent; what makes one a threat modeller and another a validator is which skill file it's told to read. There are 13, in three tiers:

  • 3 entry workflows: security-scan, security-diff-scan, deep-security-scan. One per mode, and code picks which one the parent reads: --mode chooses standard or deep, and a diff target forces the diff workflow.
  • 4 phase contracts, called by name: threat-model, finding-discovery, validation, attack-path-analysis. The four phases of one scan, in order: model the attacker, find candidate bugs, check they're real, check they're reachable.
  • 6 standalone tools: fix-finding, triage-finding, track-findings, vulnerability-writeup, propose-security-hardening, define-security-policy. Follow-ups you invoke yourself: triage findings imported from other scanners, file tickets in Jira or Linear, turn a finding into a disclosure report, fix a finding, maintain SECURITY.md.

The difference between the modes is only the discovery half. A standard scan is one Codex session doing all four phases itself, once: its own threat model, one discovery pass over every file, then validation and attack-path analysis.

A deep scan changes just the middle:

  • threat-model and finding-discovery run once per worker, up to 60 fresh sessions, each starting from scratch
  • a reducer merges their candidates while the pool runs
  • validation, attack-path analysis and the report are unchanged, and still run once

Since the rewrite, the workers run the exact same repository-wide discovery procedure a standard scan runs, from the same skill files. Deep mode is standard discovery repeated N times, plus a merge. The worker prompt names two skills and nothing else, threat-model then finding-discovery, and the reducer template names none at all. Everything after discovery belongs to the parent; that part comes at the end of the post, once the results have made their way back up.

The tools

Skills are what a session knows; tools are what it can do. One server binary serves every seat, in three roles:

  • The parent gets the full workbench server: about two dozen tools to start scans, read review items and candidates, record validations, attack paths and the draft, and seal the scan.
  • Workers and the reducer never see that server. Their config still lists it, but with enabled: false, so they can't start scans or touch run state. Instead, the coordinator launches a second, private copy of the same binary for each session, in a stripped-down "artifact writer" mode, and tells it through environment variables which folder it owns and which role the session plays. It then registers only that role's tools: three for a worker (list its files, record its threat model, record its candidates), two for the reducer (fetch its inputs, record the reduction).
  • 29 more tools on the workbench server are tagged app-only, the desktop app's surface for setup, triage and remediation. No model can see them.

Behind it all is one SQLite file, workbench.sqlite3. The tools write it, and the loop reads its counters from it. No model writes a scan artifact by hand: workers and the reducer run read-only sandboxes, so their only write path is a tool that validates the input first.

The loop

The parent calls start_codex_security_deep_scan. Everything below happens inside that one tool call, and no model is driving it. Behind the tool sits a scheduler in the MCP server process, plain JavaScript over SQLite.

There are four controls for the loop. workers is how many sessions run at once, half your cores capped at 6. subagents is how many nested threads each worker may spawn, default 3. max_discovery_runs caps total dispatches at 60. stop_after_no_new, default 6, is the convergence threshold: once that many consecutive workers have contributed nothing the reducer hadn't already seen, the run is saturated. The streak counts workers, not merges.

The loop itself is what the figure shows. Keep workers sessions running, buffer their output as they finish, merge beside the pool. After each merge the coordinator counts the canonical candidates that trace only to just-merged workers; zero extends the streak, anything else resets it. The run ends saturated or capped, both recorded in SQLite, and an agent that tries to close early gets an error:

Deep Scan cannot finish saturated before reaching its no-new-findings threshold.

Retries belong to the coordinator too. Before one it archives the failed output to attempts/attempt-NN/ and restores the previous baseline, so a merge that dies halfway can't leave the canonical set half-written.

The worker instructions

Every discovery worker gets the same prompt. The template is rendered per session, and since the rewrite the only field that differs between worker 1 and worker 60 is its label. Same target, same scope, same subagent budget. (A worker that fails validation gets its own errors appended on the retry, so this holds for first attempts.)

It also gets the whole file list. Not a shard, not a slice, not an assigned area: the coordinator copies the full inventory into every worker's folder before the session starts, and rejects the worker afterwards if its copy isn't byte-identical to the shared one. Twelve workers over a 400-file repo is 4,800 file reviews, and that repetition is the point. Nothing is divided up, so nothing falls in a gap between two workers.

What varies is the threat model. The first thing each worker does is write its own threat model, then run discovery against it. Twelve workers reading the same files through twelve independently derived threat models is where the variance reduction comes from.

The prompt opens with identity, stated as a negation:

You are one independent discovery worker inside a Codex Security Deep Scan. You are not the top-level coordinator.

Then a JSON blob holding everything session-specific, then the method: invoke threat-model and record the result, invoke finding-discovery over the assigned files, and record the complete candidate set in exactly one tool call. An empty set is fine; the template calls it "a valid discovery result".

The output contract got thinner in the rewrite. Workers used to file a terminal receipt per file: full_file_reviewed: true with an evidence note, or an explicit deferral with a reason, so "I found no vulnerabilities" and "I didn't look" produced different output. That machinery is gone from deep scans; it survives only in the diff-scan path. What's left is the instruction to review every file, and a byte-check on a file list the worker never edits. (A commit later the same day added self-reported progress counts per batch โ€” a progress bar, not evidence.)

Isolation went the other way, from prompt rules to walls. A worker session now runs with a read-only sandbox, and its writes happen through a private artifact-writer MCP server bound by environment variables to that worker's own output folder. The main workbench server stays disabled, so a worker can't touch run state or call completion. Reads are still a rule rather than a wall: nothing stops a worker reading a sibling's output except one sentence in the prompt, "Do not access another worker's artifacts."

Workers never talk to each other, so the duplicate work is never coordinated away: no worker knows what another found. The one thing that travels between them is the threat models, and they travel upward. The parent later merges the ordered worker threat models into the single canonical one that validation runs against. A worker doesn't even grade its own run. It stops once its candidate set is accepted, and "the coordinator validates discovery and writes the worker result".

The bottom layer

At the very bottom are the nested threads a worker can spawn inside its own session. A worker facing a 400-file inventory doesn't have to read it alone: it can hand batches of files to up to subagents child threads, default 3, and review in parallel. What to delegate and how to split it is the worker's call โ€” the one scheduling decision in the whole system that a model makes.

Whatever comes back stays inside the worker. The children report to the worker that spawned them, the worker folds their candidates into its own set, and it still makes the single record call itself. From the coordinator's side, nested work is invisible: it sees one worker, one threat model, one candidate list.

The old version regulated this layer tightly: one subagent owned one worklist row or a shard of at most five files, and the worker had to reject any result that came back without full-file receipts. The rewrite dropped all of that. What's left is the cap and one sentence in the prompt, "Keep any nested discovery work within the supplied subagents limit and wait for it to finish." (That's also what "model-native workers" means in #270: worker sessions no longer pin Codex's multi-agent feature flag, just a plain thread ceiling, so they run on either agent runtime.)

What the reducer is told

The workers produce candidates; the reducer merges them. It's one more Codex session, running beside the pool, at most one at a time. The coordinator starts it whenever finished workers are waiting in the buffer and no merge is running. The first merge waits for two finished workers; after that a single one is enough.

Its instructions are all consolidation, and it's told to do no analysis at all:

You are the single serial semantic reducer for one Codex Security Deep Scan. Do not inspect repository code, launch subagents, validate findings, run attack-path analysis, edit the repository, or call another Deep Scan.

The job is simple: take the new batch of worker candidates and the current canonical set, in the order given, and fold the batch in. No re-merging from scratch. Two candidates collapse into one only when "fixing the canonical issue must also fix every absorbed candidate"; sharing a subsystem, a CWE, or a sink family is not enough.

It doesn't read the ledgers with shell commands either. Its whole interface is the two reducer tools from the tools section: get_codex_security_deep_reducer_inputs to fetch, record_codex_security_deep_reduction to hand back.

The reducer hands back the updated set and its merge records, and the coordinator does the counting: it walks the merge provenance, derives the new-finding count, and updates the streak. The prompt says so directly: "Do not report or estimate newFindings; the caller derives convergence from the validated canonical provenance."

The way back up

When the loop stops, everything the scan knows sits on disk: one threat model per worker, the reducer's canonical candidate list, and a terminal manifest recording why discovery ended, saturated or capped. The tool call the parent has been blocked on all day finally returns and hands it the path to that manifest.

The parent now runs the rest of the scan itself, exactly once, following the same post-discovery steps a standard scan uses:

  1. Merge the ordered worker threat models into one canonical threat model.
  2. Run validation over the merged candidates: is this actually a bug, on the evidence?
  3. Run attack-path-analysis over the survivors: can an attacker actually reach it?
  4. Record the finished draft, findings plus coverage plus threat model, through one workbench tool.

The model never writes the report. It records the draft as structured JSON, then calls one completion tool; the workbench checks the contract, seals it, and generates report.md from the sealed data. The deep-scan skill states it flatly: "Do not author report.md directly." The prose can't contradict the findings when the prose is generated from them.

Per-finding write-ups and a hardening pass used to be mandatory steps here; since the rewrite, they run only when the user asks for them, using two of the standalone skills from earlier, vulnerability-writeup and propose-security-hardening.

Summing it up

None of that machinery is really security-specific. The performance comes from two things. Forcing agents through every single file, deterministically, with the bookkeeping outside the model. And launching a lot of agents: they're stochastic, so 60 with the same instructions give more coverage than one ever would.

Soon we'll release an open-source version, benchmarked and a bit updated, that works with any provider. Stay tuned.