Rules: what may leave the sandbox
Lesson 4 ended with a question: what about the one command that genuinely needs to leave the boundary? A rule is how you name it, and only it.
Rules live in a .rules file inside a rules/ folder next to a config layer, for example ~/.codex/rules/default.rules. They are experimental, and the docs say so.
prefix_rule(
pattern = ["gh", "pr", "view"],
decision = "prompt",
justification = "Viewing PRs is allowed with approval",
match = [
"gh pr view 7888",
"gh pr view --repo openai/codex",
],
not_match = [
"gh pr --repo openai/codex view 7888",
],
)Read it in four parts. The pattern is an exact command prefix. The decision is what happens when it matches. The justification is what you will be shown when it does. And the two lists are inline tests: examples that should match, and examples that should not.
Those test lists are the point
A rule is a security boundary written as a string match, which is exactly the kind of thing that is wrong in a way nobody notices. The not_match example above is the lesson: gh pr --repo x view 7888 does not match, because the pattern has to be an exact prefix and the flag moved.
Write the awkward cases into not_match as you think of them. It is the cheapest review your rules will ever get.
The decisions
| Decision | What it does |
|---|---|
allow | The default. The command may run |
prompt | Ask before running it |
forbidden | Block it without asking |
When more than one rule matches, Codex applies the most restrictive decision. So a forbidden rule cannot be undone by a broader allow, which is the behaviour you want from a deny list.
["gh", "pr", "view"] is a rule about reading pull requests. ["gh"] is a rule about anything the GitHub CLI can do, including closing and merging.- Write a rule for the one command your workflow needs outside the sandbox.
- Add a
not_matchline for the version of that command you would not want allowed.
You understood something today that you didn't yesterday.