codex/sandbox-state-meta missing sandboxPolicy
Read what the message actually says. A record named codex/sandbox-state-meta
arrived somewhere without a field called sandboxPolicy, and whatever received
it requires that field. Nothing was blocked, no kernel refused anything, no
permission was evaluated and denied. It is also not an API error: no key, no
quota, no model and no network call appears anywhere in that sentence. The word
“sandbox” is in the name of a data structure, not in the description of what
went wrong.
The word “sandbox” is pointing you at the wrong repair
The reflex, reasonably, is to go and change sandbox settings — switch the approval mode, pick a more permissive policy, reach for whatever bypass flag the CLI offers. None of that can help, and the reason is mechanical: the complaint is that a field is absent from a message, not that its value was rejected. Setting a policy to something more permissive still produces a record that either carries the field or does not. You can spend an hour widening your sandbox configuration and arrive at the identical error with a weaker setup than you started with.
The right question is not “what should my sandbox policy be”. It is which two components are exchanging this record, and are they from the same release.
The shape of the name is the clue. codex/sandbox-state-meta reads as a
namespaced protocol method — the kind of message a core process sends to a front
end, or a client sends to a server. Codex runs in more than one configuration:
a CLI you invoke directly, an editor extension driving a core process, an
app-server style client, an SDK consumer, or something you wrote yourself
against the protocol. Anything that pairs two independently updated halves is
the classic producer of this failure: an extension that updates itself on its
own schedule against a CLI you update by hand will, sooner or later, be a
release apart at the moment one side starts requiring a field the other does not
send.
The other producer is persisted state. A session or state file written by one build and read by a newer one carries whatever the older writer knew how to write — which will not include a field added after it was written.
Both of these are version and pairing problems. Neither is a permission problem.
Is retrying useful?
No. A message either carries the field or it does not.
Relaunching the same pair of components regenerates the same record with the same gap. There is nothing transient to wait out and no backoff that changes a schema.
One action is worth exactly one deliberate attempt, and it is not a retry: restart both halves, not just the one you were looking at. A front end often keeps a core process alive across window reloads, so closing and reopening your editor can re-attach to the very same stale process — which looks like a retry and is actually the same run continuing. Stop both, start both, once. If it comes back, you have a real mismatch and the rest of this page applies.
Finding which pair is mismatched
Each of these is a single observation that eliminates a branch.
Reproduce with the plain CLI, no editor attached, in a clean directory. If it still happens, both ends of the exchange are inside the CLI and its state. If it does not, the front end is one end of the exchange, and you have halved the search space with one command.
Try a fresh session or fresh state. If a new session works and an existing one does not, the record is coming from state that an older build wrote. That is a decisive result: it rules out your configuration entirely, because the same configuration works in the new session.
Check whether more than one build is installed. command -v -a codex shows
what your shell resolves; your editor extension may be configured to launch a
different path entirely. Two installs, one updated and one not, produce this
error while every version check you run by hand looks correct — because you keep
checking the one that is fine.
Ask whether it started right after an update. If one side updated yesterday and the other did not, the investigation is over.
If you are speaking the protocol yourself, you are one end of the exchange. The other end’s expectations changed under you, which is what an unpinned dependency on an internal protocol does.
Fixes, matched to what you found
Mismatched halves — bring both to the same release and restart both processes, not just the visible window. Then keep them together: updating the extension and the CLI independently is the mechanism that created the problem, so pair the upgrades from now on.
State written by an older build — start a fresh session and let the newer build write its own state. Do not hand-edit the record. Its internal shape is not a stable public contract, it changes between releases, and a field layout copied from a forum post will be wrong in a way that is much harder to diagnose than a missing field.
A client you wrote or an SDK integration — emit the sandbox policy
explicitly in the record you produce, and pin the protocol or package version
you built against so a silent upgrade cannot change the contract underneath you.
Take the current field shape from the Codex documentation
(learn.chatgpt.com/docs, indexed from developers.openai.com/codex/llms.txt),
not from a third-party snippet — this is exactly the sort of internal detail
that third-party write-ups capture once and never update.
An environment where you cannot update the front end — run the CLI directly until you can. Call it what it is: a workaround that costs you the editor integration, not a fix.
What not to do: do not reach for a bypass or “dangerous” mode to get past it. It cannot supply a missing field, and it silently removes the boundary that the rest of this cluster exists to protect. There is a general rule hiding here worth taking away: before disabling a sandbox to make an error disappear, check that the error is about the sandbox. This one is not, and the two errors next to it in this cluster — which genuinely are — deserve a more careful decision than this one does.
Confirming it
Get the tool to the point where it reports which sandbox mode it is operating in and then executes one trivial command that produces visible output. Reaching that point proves the record was exchanged and accepted, which is more than the absence of an error message proves.
Then repeat from cold: both halves stopped, a fresh session started, ideally in a different directory. Warm processes are the reason this error appears to come and go. If it works warm and fails cold, you have not fixed anything — you have been testing a process that was started before your change.
Related failures in this cluster
The other two errors near this one are real sandbox failures, and the contrast is the fastest way to tell where you are. An unknown bubblewrap option is a binary too old to understand the command line it was given. A loopback EPERM inside the namespace is the kernel refusing a capability, and it is the one where the environment — container, CI runner, restricted kernel — decides the outcome. Both of those fail before your command runs; this one fails while two pieces of software talk to each other. If the message in front of you mentions sandboxes and you cannot tell which of the three you have, the AI coding error triage tool sorts them by what had already happened at the moment of failure.