OpenAI Codex · Code Mode

execpolicy: Let the Policy File Carry Its Own Tests

A whitelist gone wrong usually only shows up in production. Codex writes the examples into the rule itself and runs them on load. If the rule and the examples fight, that policy never enters the session.

Course goalAfter this lesson you can explain two things. How a command is split after it enters the policy, how it hits by prefix, and how it ends up allow, prompt, or forbidden. And why the examples next to a rule can pin down false hits at load time.
Try it first · One command through the policy
One policy, four commands, three writings
Cmd
Safe, dangerous-flagged, looks dangerous, wrapped as a disguise. Pick one, then play.
Policy
1 SplitWaiting
2 Load checkWaiting
3 Prefix matchWaiting
4 Strictest callWaiting
argv this step sees
No tokens yet.
Tests the policy carries
Positive must hitNot run
git reset --hard
Negative must missNot run
git reset --keep
Hit Play to see how this command is split, which rule it hits, and what grade it gets.
Logic trail · each animation step maps to a stretch of source
  1. Read the policy text and start parseparser.rs L57
  2. Starlark evaluates prefix_rule; missing decision defaults to allowparser.rs L348
  3. Rules and examples go onto the pending-check queueparser.rs L405
  4. Run negatives first; a hit reports ExampleDidMatchparser.rs L145
  5. Then positives; a miss reports ExampleDidNotMatchparser.rs L147
  6. Wrapped commands are split into inner argv firstexec_policy.rs L831
  7. Exact-lookup the rule bucket by the first tokenpolicy.rs L334
  8. Multiple rules or a compound command take the strictestpolicy.rs L403
  9. The call maps to Skip, NeedsApproval, or Forbiddenexec_policy.rs L375
Hit Play to see how a command clears the policy, and how built-in examples pin the call down.
It dies at load timeWhen the prefix is too short or the negative is wrong, none of the four commands get a chance at allow or forbidden. The policy hasn’t been handed over; the bad example already aborted load.
Why --keep survivesThe correct rule writes the pattern as three tokens. The third token doesn’t match --keep, so the ban stays dark. That isn’t a heuristic miss — the author pinned the boundary with a negative.
Wrapping doesn’t save itWrap it in bash -lc: if it splits, judge the inner argv. Only a failed split falls back to the whole line. This demo wrap does split, so it’s still forbidden.
Teaching sketch: the stage only demos two sample rules — ban git reset --hard, allow ls. No real shell is run. Line numbers on the logic trail match openai/codex commit 4f39251a01.
Idea 1 · Keep the rule and the examples in one place
What problem it solves

You write a ban for the team, pattern as git plus reset. You meant to stop --hard. A week later someone reports --keep is blocked too. Shorten the prefix and anything after it hits the same rule.

Another week on, the model wraps it in bash -lc. The ban matches literal argv against bash, the first token misses, and the command slides into the heuristic path.

Every whitelist line is betting two things. One, the pattern covers what you meant to stop. Two, it won’t false-hit what you meant to let through. You usually only learn that in production. The next person who edits the pattern also can’t see which command the author was afraid of clipping.

What the idea is

Codex writes the examples into the rule itself. match is the command this rule must hit; not_match is the one it must miss. prefix_rule doesn’t run examples on the spot — it queues the rule and the examples. After the whole Starlark file evaluates, parse checks them together.

Order is fixed. Negatives first, then positives. A false hit on a negative reports ExampleDidMatch. If no positive hits, it reports ExampleDidNotMatch. Either error makes parse fail, and Policy is never builded out.Source: codex-rs/execpolicy/src/parser.rs lines 57–78 and 133–151

The crate README puts it in one line: they are sample invocations checked at load time, and you can treat them as unit tests. Both the string form and the token-array form become a token list in the parser; strings go through shlex. During the check the heuristic callback is empty, so a positive must hit a real prefix rule — it can’t ride a heuristic.

Starlark eval Attach rules and examples Negatives first Must hit none Then positives Must hit at least one Hand over Policy Load failed If examples don’t line up, it stops before the policy is handed over The error carries the prefix_rule call’s line and column
Teaching timeline: eval only collects; the check happens before build.
Why it lasts

The typical whitelist failure is the author thinking the pattern means one thing. Write that belief down as examples and the machine can disagree. This doesn’t depend on Starlark — copy it into JSON or YAML and it still holds.

Keep the rule and the examples together, and when the policy file is copied into a user directory or shipped as an overlay, the examples travel with it. The loader has no branch that reads rules and skips examples. Skipping examples is skipping tests. The loader won’t invent examples for you; if you wrote them, it will enforce them.

Idea 2 · Prefix match; multiple rules take the strictest
What problem it solves

A regex can say git reset plus anything, and it can also write exceptions the author can no longer read. If stacked rules take the widest, one loose user-level allow can cover a system-level forbidden.

What the idea is

The rule body is a prefix. Matching is exact string equality — no glob, no regex. git reset --hard will eat a command that then adds origin/main, because extra tokens don’t join the compare. It will not eat a --config stuffed in the middle, because the second token misses.Source: codex-rs/execpolicy/src/rule.rs lines 46–59

Rules go into buckets by the first token. Lookup exact-matches argv[0] first. If that misses, it may fold an absolute path down to a basename. On multiple hits it takes max of decision. Decision derives Ord; variant write order is severity: Allow < Prompt < Forbidden.

Compound commands are split, flattened, then maxed again. One segment git status is Prompt, one git commit is Forbidden, and the whole pipe is still Forbidden.Source: codex-rs/execpolicy/src/policy.rs lines 265–287 and 402–411

git prefix decision = prompt git commit prefix decision = forbidden git commit -m hi Both enter matchedRules max = Forbidden Allow < Prompt < Forbidden Stacking can only tighten. The order lives on the enum variants; there is no second priority table
Teaching contrast: one command hits two rules; the outside only sees the stricter one.
Why it lasts

Stacking can only tighten, never loosen. That order lives on the enum variants; runtime has no second priority table to get wrong. Rewrite it in another language: three strings as an ordered enum, one max to fold them.

A prefix forces you to land intent as a token sequence. The cost: a flag stuffed in the middle kills the rule, so the author must shorten the prefix or write another. The examples are there to catch that cost: write it too short and the negative dies at load time.

Idea 3 · Unwrap first; if it won’t unwrap, fall back to the whole line
What problem it solves

The ban is written as git reset --hard. The model wraps it in bash -lc. Match literal argv and the first token is bash — the ban stays dark.

What the idea is

Before judging, it runs parse_shell_lc_plain_commands. The script may only be plain-word commands plus &&, ||, semicolons, and pipes — no redirects, substitutions, parens, or control flow. Once it passes, command nodes become argv segments. bash -lc wrapping git reset --hard splits into those three inner tokens, then those go to prefix match.Source: codex-rs/core/src/exec_policy.rs lines 831–858

If it won’t split, the whole argv is one command, handed to heuristics and the later sandbox. Empty quotes in an argument can still be recovered. Empty quotes in the command name fail word-only parse, the prefix rule never sees git, and the command drops into heuristics.

If you can’t swallow the script, don’t pretend you already understood it.
Why it lasts

The parser admits what it can’t swallow, so it doesn’t pretend it already understood the script. That’s the generic fail-closed shape. It stops a successful parse that dropped a dangerous segment. After a failed split, heuristics and the sandbox are still there.

Side-by-side · Another answer to the same question

DeepSeek Harness: two knobs and a dropdown

DSH doesn’t write command-level patterns. Permission presets bundle sandbox mode and approval policy. The default table has two rows: workspace-write with ask, danger-full-access with never. Approval policy itself is only ask and never.

The knobs are easy. You cannot write a preset that only bans git reset --hard and leaves other git alone. That exception either goes to the escalate-after-sandbox-deny path, or to a custom knob combo shown under the reserved name custom. The dropdown covers everyday switches; the long tail of named bans it cannot cover.

Both sides verified against source · 2026-08-22 · DSH · Approvals & Permissions

Claude Code: tool name plus an optional-content allowlist

A rule string looks like Bash, or Bash(npm install), or Bash(git *). The parser splits tool name and content on the parens. Shell rules then split into exact, prefix, and glob.

Search match, not_match, example as rule fields: there is no load-time example check. Once git * is in allow, git reset --hard is eaten by that glob unless you write a more specific deny. Codex pins exceptions at load time with a shorter exact prefix plus a negative. Claude Code leaves exceptions to rule stacking order and a runtime confirm.

Both sides verified against source · 2026-08-22
Classroom Exercise
01

Prefix too short: load or judge?

Shorten the ban on git reset --hard to git plus reset, and leave not_match as --keep. At load, do you get ExampleDidMatch or ExampleDidNotMatch, and does the command still get a chance to be judged?

A harder follow-up: under that same bad rule, does ls -l still get allow first? In the demo, switch the policy to “Prefix too short” and play it again — check it against your reasoning.

Takeaway:Keep the rule and the examples in one place, and run them on load. A positive must hit, a negative must miss; if they fight, refuse to load. Prefix compare is exact; multiple rules take the strictest. Unwrap first; if it won’t unwrap, fall back to the whole line.