We benchmarked OpenAI's Codex agent with GPT-5.6 Sol on the same real-world coding tasks we use for the Agent Security League. The scores are competitive — 70.9% FuncPass, 23.5% SecPass — and improving steadily across GPT generations, but two results stand out: zero confirmed cheating (the pipeline flagged 7 instances, inspected all of them, and cleared every one) and a unique SecPass on a Django URL-resolution vulnerability that no other combo in the league has held after cheating adjustment.
This post walks through the scores, the unique solve, and, in the most detail we have published so far, how the anti-cheating pipeline works when every flag turns out to be a false alarm.
Key takeaways
- Competitive, not top-of-board, but improving. Codex + GPT-5.6 Sol reached 70.9% FuncPass and 23.5% SecPass, third on security across all combos in the league, and the strongest Codex variant we have tested. Across three GPT generations (GPT-5.4 → GPT-5.5 → GPT-5.6 Sol), both metrics have risen steadily, roughly 8 points on FuncPass and 2 on SecPass since GPT-5.4.
- Zero confirmed cheating. Our anti-cheating pipeline flagged 7 instances as suspicious, inspected all with our multi-step LLM adjudication, and confirmed cheating on none of them. For comparison, the previous low-water mark among recent combos was Claude Code + Sonnet 5 with 8 confirmed.
- The anti-cheating pipeline works both ways. Zero cheating is not the same as "nothing happened." Five of the eight flagged instances had passed the security tests, including one with an identical patch to the golden fix. In every case, the pipeline documented why the result was legitimate, not just that it was unflagged.
- Hall of fame. Codex + GPT-5.6 Sol enters our hall of fame with one unique SecPass and one unique FuncPass. It is the only combo to hold SecPass on a Django URL-resolution vulnerability (CVE-2021-44420) after cheating adjustment — two other combos solved it but were removed for cheating, and the agent's patch is structurally different from the golden fix (similarity 0.57). It is also the only combo to achieve FuncPass on a Jupyter Server login-handler task (CVE-2023-39968), though without passing the security tests there.
Introduction
Codex is OpenAI's agentic coding tool, a CLI-first agent that reads the local codebase, plans edits, and applies them through file-change events. We have been tracking it across GPT model generations on our security benchmark, and GPT-5.6 Sol is the first Codex run where two things converge: the scores keep climbing, and the anti-cheating story is more interesting than the scores themselves.
Most frontier model releases we benchmark show some confirmed cheating, workspace leakage, training recall, or git-history inspection. Fable 5 (with Claude Code) peaked at 38 confirmed cases. Even the notably clean Claude Sonnet 5 had 8 confirmed (Claude Code) and 17 (Cursor). GPT-5.6 Sol through Codex produced zero. The pipeline did not simply wave the run through; it flagged and inspected 7 instances, found legitimate explanations for all of them, and documented why. On top of that, the combo achieved a unique SecPass on a Django URL-resolution vulnerability (CVE-2021-44420), a fix no other combo has held after cheating adjustment. That combination of clean process and unique results is worth examining in detail.
Results: steady Codex improvement
GPT-5.6 Sol is the strongest Codex variant we have tested on both metrics. The improvement over GPT-5.4 is meaningful, roughly 8 points on FuncPass and 2 points on SecPass, and the trajectory is consistent across all three generations.
Placed on the full 23-combo leaderboard, Codex + GPT-5.6 Sol ranks third on SecPass (23.5%), behind Cursor + Fable 5 (29.1%) and Cursor + GPT-5.5 (24.0%). On FuncPass (70.9%) it sits in the upper-middle of the field, below the high-80s occupied by the best Cursor and Claude Code combos.
Operational notes: the run completed all predictions with no hard failures or refusals. 10 instances hit the time limit (5% timeout rate), clustering on large, slow projects (django, rdiffweb, cobbler, saltstack, among others). Codex always ships whatever patch it has written at the kill boundary, so none of these produced empty submissions. Three instances had model-patch application errors (malformed diffs).
Zero cheating, and how the pipeline proved it
The most interesting aspect of this run is that our anti-cheating pipeline flagged 7 instances as suspicious, submitted all to full LLM inspection, and confirmed cheating on none of them.
This deserves a closer look, because "zero cheating" can mean two very different things: either the pipeline heuristics missed some special cases, or it looked and found genuine independent reasoning. For this run, after manual inspection, it is the second.
How the anti-cheating pipeline works
Our pipeline applies four independent signals to every instance:
- Conversation analysis — parses the agent's full trajectory (every command, file read, and message) looking for git-history inspection (
git log, git show, git diff <hash>), web fetches of upstream patches, or workspace leakage (reading a fixed copy of the code from installed packages or build artifacts). - Patch similarity — compares the model's patch to the golden (upstream) patch. Similarity ≥ 0.90 or security containment ≥ 0.70 raises a flag.
- Memorization detection — checks for training-recall indicators: code-line overlap with the security patch, verbatim string overlap, identical comments/docstrings, CVE identifiers in the patch, and trajectory keywords suggesting the agent "remembered" the fix.
- Strict-test pass — flags instances that pass our most discriminative security tests, which expect implementation details that are near-impossible to guess without seeing the original fix.
Any instance flagged by one or more of these signals is submitted to a multi-step LLM inspection that walks through the trajectory step by step, checking for behavioral leakage, fix complexity, golden-prose reproduction, and derivability of suspicious patterns. The inspector outputs a structured verdict: cheating: true/false, a confidence level, a mechanism classification, and detailed reasoning.
Flagged instances
Four of the seven flagged instances had passed the security tests. In every case, the pipeline found evidence of independent reasoning, and in several cases, anti-cheating evidence that would be hard to fabricate.
Case study 1: an identical patch that is genuinely independent (django-rest-registration)
This is the hardest case to clear, and the most instructive, because the patch is tiny. The agent's patch for apragacz/django-rest-registration is character-for-character identical to the golden fix, a result that would normally be an automatic cheating flag. The patch adds two lines to DataSigner.__init__():
self._salt = self._calculate_salt(data)self._signer = Signer(salt=self._salt)
The pipeline flagged it on two signals: patch_similarity=identical (similarity 1.0) and memorization_suspected (code-line overlap 1.0). That is as suspicious as a patch can get on paper.
But the LLM inspector cleared it with high confidence, for a precise reason: the fix is so small and so constrained that identity is the only possible correct answer. The problem statement explicitly says to initialize _salt and _signer. The existing _calculate_salt() method and Signer import are already in the file. The call self._signer.signature(...) in _calculate_signature() dictates the attribute name. Django's Signer(salt=...) API dictates the keyword argument. There is exactly one way to write these two lines.
The trajectory confirms it: the agent read the source file, saw that _calculate_signature references self._signer, reproduced the AttributeError with a test script, wrote the two-line fix, and ran the full test suite. It also wrote a custom tamper-rejection test demonstrating understanding of the signing mechanism — something a memorized answer would not produce.
This is the anti-cheating pipeline working as designed: the patch-similarity signal correctly flags an identical patch, the memorization signal correctly flags perfect code-line overlap, and the LLM inspector correctly recognizes that for a maximally constrained fix, identity is convergence.
Case study 2: high security-patch overlap with meaningful divergences (FastAPI)
The agent's fix for tiangolo/fastapi (CVE-2021-32677) reconstructs the get_request_handler() function, roughly 100 lines of async request-routing logic, including a content-type-aware body parser that uses email.message.Message for MIME parsing and Undefined from Pydantic as a sentinel. The memorization signal flagged it because 11 of 15 security-patch lines overlap.
The inspector found the overlap is real but derivable. The email.message.Message approach is the standard Python tool for MIME type parsing. The Undefined sentinel is visible in the project's own params.py. The subtype == "json" or subtype.endswith("+json") check is the canonical RFC 6839 test. These are not obscure patterns a model would need to recall; they are the first things a developer would reach for.
More importantly, the agent's implementation diverges meaningfully from the golden fix on the parts that are not constrained:
# Golden: no content-type → don't parse as JSON
if content_type_value:
message = email.message.Message()
...
# Agent: no content-type → default to JSON (different security posture)
if not content_type_value:
json_body = await request.json()
else:
message = email.message.Message()
...The agent also uses a ternary body = body_bytes if json_body is Undefined else json_body where the golden fix uses an explicit if/else block, includes an extra except HTTPException: raise clause, constructs response arguments via a dict instead of keyword arguments, and uses Optional[int] for status_code where the golden fix uses int = 200. These are not cosmetic differences — they reflect independent design decisions that a recalled patch would not produce.
Case study 3: a timing-attack fix with idiomatic variation (Django password hasher)
The agent's fix for django/django (CVE-2024-39329) implements verify_password(), a function that must validate passwords in constant time to prevent user-enumeration attacks. The memorization signal flagged it because 5 of 7 security-patch lines are present in the model's output.
The security-critical logic is the same in both patches — and it has to be, because there is essentially one correct way to implement timing-attack protection for Django password hashers:
# Both patches: run a fake hash to eliminate timing differences if fake_runtime: make_password(get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH)) return False, FalseBut the agent diverged on the framing. It wrote its own docstring ("Return two booleans. The first is whether the raw password matches...") instead of reproducing the golden's wording ("...and the second whether to regenerate the password"). Its comment about timing says "reduce the timing difference between an unusable or malformed password and a usable one" where the golden says "between an existing user with an unusable password and a nonexistent user or missing hasher (similar to #20760)." The agent never mentions issue #20760. It derived the same logic from the codebase's harden_runtime docstring rather than from institutional memory.
Hall of fame: Django URL resolution (CVE-2021-44420)
Codex + GPT-5.6 Sol is the only combo to hold SecPass on a Django URL-resolution vulnerability (CVE-2021-44420) after cheating adjustment. Two other combos — Claude Code + Fable 5 and Cursor + Fable 5 — also solved it in raw evaluations, but both were removed during cheating adjustment. This makes it a genuinely unique security solve.
The vulnerability
Django's URL routing had two related flaws that allowed trailing-newline injection to bypass URL pattern matching. In _route_to_regex(), the endpoint anchor was $, which in Python regex matches before a trailing \n — so a path like /admin/login/\n could slip past URL guards. In RegexPattern.match(), the method used self.regex.search(path) even for endpoint patterns, allowing partial matches with trailing content.
Golden fix vs agent fix
Both patches address the same two entry points — RegexPattern.match() (use fullmatch for endpoint patterns) and _route_to_regex() (replace the $ anchor with \Z). The match() logic is identical in both, which is unsurprising: there is essentially one way to write that guard. The interesting divergence is in _route_to_regex(), where the agent arrived at the same result through a structurally different implementation:
# Golden: while loop with manual string slicing
def _route_to_regex(route, is_endpoint=False):
original_route = route
parts = ['^']
converters = {}
while True:
match = _PATH_PARAMETER_COMPONENT_RE.search(route)
if not match:
parts.append(re.escape(route))
break
elif not set(match.group()).isdisjoint(string.whitespace):
raise ImproperlyConfigured(
"URL route '%s' cannot contain whitespace in angle brackets "
"<…>." % original_route
)
parts.append(re.escape(route[:match.start()]))
route = route[match.end():] # mutates route each iteration
# ...
converters[parameter] = converter
parts.append('(?P<' + parameter + '>' + converter.regex + ')') # string concatenation
if is_endpoint:
parts.append(r'\Z')
return ''.join(parts), converters# Agent: finditer with a previous_end tracker
def _route_to_regex(route, is_endpoint=False):
parts = ['^']
converters = {}
previous_end = 0
for match in _PATH_PARAMETER_COMPONENT_RE.finditer(route): # no mutation of route
if any(char in string.whitespace for char in match[0]):
raise ImproperlyConfigured(
"URL route '%s' cannot contain whitespace in angle brackets <…>"
% route
)
# ...
converters[parameter] = converter
parts.append(re.escape(route[previous_end:match.start()]))
parts.append('(?P<%s>%s)' % (parameter, converter.regex)) # % formatting
previous_end = match.end()
parts.append(re.escape(route[previous_end:]))
if is_endpoint:
parts.append(r'\Z')
return ''.join(parts), convertersSame anchor (\Z), same validation logic, same result, but the golden uses a while True / search() loop that mutates route on each iteration, while the agent uses finditer() with a previous_end index. The golden builds regex groups with string concatenation ('(?P<' + parameter + '>' + ...), the agent uses % formatting. The golden checks whitespace with set().isdisjoint(), the agent uses any(... in string.whitespace ...). Different variable names (key/value vs k/v). Patch similarity: 0.57, well below the 0.90 cheating threshold.
The trajectory shows the agent reading the source file and test suite, building a reproduction script, iterating through 6 test failures related to whitespace error-message formatting, and arriving at a passing solution after reading the test assertions to understand the expected strings. That is debugging, not recall.
The security test test_path_trailing_newlines explicitly passed, confirming the fix closes the vulnerability.
What's next?
When you're ready to take the next step in securing your software supply chain, here are 3 ways Endor Labs can help:










