The Codex Changelog
All Episodes
Strict Guardian Breaks: Detect TooManyDenials in Codex

Strict Guardian Breaks: Detect TooManyDenials in Codex

0:00|0:00

Learn how the new auto_review.circuit_break_action setting adds a structured tooManyDenials signal when Guardian trips a denial limit, so orchestration code can distinguish policy lockouts from ordinary interruptions. We also cover how to wire the check into retries, audits, and manual review, plus the key caveats for older clients and event handling.


Chapter 1

The Denial Limit Gets a Name

Ethan Park

Picture a headless Codex run in CI that stops halfway through a task. Your orchestrator logs it as an interruption, shrugs, and retries. The agent hits the same wall, gets denied the same way, and stops again. Thanks to Jellypod for helping make this daily show a reality. Today is about a setting that gives that second stop a name your code can read.

Maya

The setting is auto_review.circuit_break_action, and you can now set it to "strict". The default value is "default". It lives under an auto_review table in config.toml. It came in through pull request 48796, titled "Add opt in structured errors for Guardian circuit breaker interruptions," which shows up in the 0.159.0 changelog list. The app server README documents the behavior.

Maya

Here's the mechanism. Guardian is the auto review layer that evaluates actions the agent wants to take. Each denial counts toward a limit. When a turn reaches that denial limit, the circuit breaker trips and the turn is interrupted. With strict set, Codex attaches a TooManyDenials error to TurnAborted.error. In app server notifications and saved history, that shows up as turn.error.codexErrorInfo equal to the string tooManyDenials.

Ethan Park

And the default is the important contrast. With "default", that error field stays unset. Both modes still preserve the warning, the denial limit, and the interrupted status. So nothing disappears in default mode. The turn still ends as interrupted with the warning. What's missing is a field that says why.

Ethan Park

Why does that matter? Think about what else produces an interrupted turn. A person hits control C. A network call times out. A connection drops. And now Guardian has locked the agent out after repeated denials. Those need different responses. A control C or a dropped connection is often safe to retry. A Guardian lockout means the agent kept proposing things your policy refuses, whether that's writes outside the allowed directories, unapproved network access, or binaries you haven't permitted. A blind retry just burns time and money to reach the same lockout.

Maya

That's the failure pattern with codex exec in a pull request bot, or a supervisor agent running child agents. Without a discriminator, the supervisor can't tell a policy lockout from a flaky network. It either retries forever, or it fails opaquely and nobody on the security side finds out. With strict on, you branch on a single field.

Maya

Setup first. In your user config at ~/.codex/config.toml, or a repository level .codex/config.toml, add the table header auto_review and under it circuit_break_action equal to "strict". That's the whole change. Then, in whatever consumes app server notifications, look at the aborted turn. The shape from the protocol is a turn with status interrupted, and an error object whose codexErrorInfo is tooManyDenials.

Ethan Park

The handler pattern is short. If the turn status is interrupted, check whether turn.error, using optional chaining, has a codexErrorInfo equal to tooManyDenials. If it does, do not retry. Notify a security reviewer with the turn ID and thread ID and a reason like policy denial limit, then escalate that thread to manual review. Otherwise, treat it as an ordinary interruption and run your normal recovery path. Two branches, and the first one protects you from the loop.

Ethan Park

You can also use it for audit. Log the discriminator with the thread ID and you get a queryable record of every time Guardian stopped an agent. Because it's saved in history too, you can look back over past threads for it, not only react live.

Chapter 2

Caveats and Smaller Changes

Maya

Now the caveats, because there are three. First, it's opt in on purpose. The pull request says older clients may not recognize the new error in shared history. If you turn on strict, make sure every client that reads those threads, including IDE extensions and your own tooling, handles an unfamiliar error value without choking. The default leaves the error unset for that reason.

Ethan Park

Second, and this one catches people: strict does not emit a separate Error event. The documentation is explicit that neither mode emits one. The information rides on the turn itself, in turn.error. So if your integration only listens for an error notification, it will never see the circuit breaker. You have to inspect the aborted or completed turn payload.

Maya

Third, scope. This fires when the denial limit is reached, not on every denial. If Guardian rejects one command and the agent keeps reasoning and picks a different approach, you won't see tooManyDenials. It's a signal that the breaker tripped, not a per command denial feed. If you want visibility into individual denials, this isn't that feature.

Ethan Park

A few smaller things from the same stretch. Pull request 50094 added stored thread attachments to the app server. The method names are thread/attachment/add, list, and remove, plus thread/attachmentOwner/list. You can attach metadata, say a canonical GitHub pull request URL, to a thread without loading or resuming it. The owner lookup works in reverse, so you can ask which threads are tied to a given pull request.

Maya

On the terminal side, 0.160.0 added X11 primary selection support. In fullscreen mode on supported local Linux X11 terminals, you can select transcript text and paste with middle click. Also in 0.160.0, Codex now reclaims unused SQLite log database pages in the background, which targets disk growth from the local logs, pull request 49069.

Ethan Park

One more, tied to today's topic. Pull request 51070 fixed the Guardian Decisions adapter. It used to reject developer messages, including host annotated trusted tool context, so requests carrying that context couldn't be classified. Now developer text annotated with TrustedTool::KIND is appended to the classification rubric. It accepts only a single text part with a single matching annotation, and it still rejects other developer messages and unsupported evidence.

Maya

So the takeaway is small and concrete. If you run Codex unattended, set circuit_break_action to strict, check your clients can read the new value, and branch on tooManyDenials in the turn error instead of listening for an error event that never comes.