Execpolicy is the third trust mechanism alongside the sandbox and approvals, and it is about specific commands. Rules govern the command prefixes Codex wants to run outside the sandbox: they deterministically decide a call's fate by its beginning rather than relying on the model's judgment. An important caveat about maturity: the format is experimental and uses the Starlark language. That means the syntax may change, and it is checked against the current documentation rather than carried over from memory.
The rule file's location obeys the same layer logic as the rest of the configuration. The file lies in the rules/ directory next to the active config layer - for example, ~/.codex/rules/default.rules for the user level or .codex/rules/ in a trusted project. This matters for the trust model: rules from the project take effect only after the workspace is trusted, exactly like the rest of the executable configuration. The location determines both precedence and who wrote the rule.
A rule is described through a command prefix and a decision, not through the full string. It helps to see such a rule in full once. Below is a prefix_rule for reading via gh pr: the pattern sets the command's beginning, decision what to do with it (here prompt), justification explains why the stop is needed. It is the prefix, not the exact string, that lets one rule cover a family of commands while staying narrow and predictable.
The key thing in a rule is the built-in unit tests via match and not_match. The match fields list the commands the rule must cover, and not_match those it must not touch. This is not decoration but a check: the not_match list catches a too-broad pattern that would accidentally grab a neighboring command. A rule without tests is easy to write past the intent; a rule with match and not_match proves for itself that it covers exactly what is intended and nothing beyond.
The decisions are ordered by strictness, and this order cannot be bypassed. forbidden is stronger than prompt, and prompt stronger than allow: if a command falls under several rules, the stricter one wins. This is the same logic as deny in permissions - a ban is not removed by a softer permission. Understanding the order of strictness dispels the illusion that a broad allow will "override" a forgotten forbidden: in fact it is the strict decision that determines the command's fate.
It helps to see how a rule is checked before application once. Below is codex execpolicy check: it runs a specific command against a set of rules and shows the decision. This is a negative test of the policy in its pure form: instead of learning about a too-broad or too-narrow rule in production, you check it in advance on real commands. It is worth checking every rule change with execpolicy check - like tests for code, only for policy.
The point of execpolicy is determinism where the model's judgment is unreliable. The model may treat the same command differently in different wordings; a prefix rule decides the same way always. So dangerous families of commands or those requiring attention - external mutations, access to the sensitive - are moved into execpolicy rather than relying on the agent to "figure it out". A deterministic policy on prefixes is a boundary that can be read, tested and explained in a review.
The typical failures around execpolicy are predictable. Writing a rule without match and not_match and missing the intent. Relying on a broad allow to override a forgotten forbidden - though the strict decision wins. Forgetting that the format is experimental and carrying over stale syntax from memory. And not running execpolicy check before application. Describe rules by prefix with tests, remember the order of strictness, verify the syntax against the docs and check every change with check.
# Starlark, a rules/ file next to the active config layer
prefix_rule(
pattern = ["gh", "pr", ["view", "list"]],
decision = "prompt", # forbidden > prompt > allow
justification = "External GitHub read requires an explicit check",
match = ["gh pr view 7888", "gh pr list --limit 10"],
not_match = ["gh issue list"],
)# A negative test of the rule before application
codex execpolicy check --pretty \
--rules ~/.codex/rules/default.rules \
-- gh pr view 7888 --json title,body,comments