fix: policy/*.codexpolicy -> rules/*.rules (#7888)

We decided that `*.rules` is a more fitting (and concise) file extension
than `*.codexpolicy`, so we are changing the file extension for the
"execpolicy" effort. We are also changing the subfolder of `$CODEX_HOME`
from `policy` to `rules` to match.

This PR updates the in-repo docs and we will update the public docs once
the next CLI release goes out.

Locally, I created `~/.codex/rules/default.rules` with the following
contents:

```
prefix_rule(pattern=["gh", "pr", "view"])
```

And then I asked Codex to run:

```
gh pr view 7888 --json title,body,comments
```

and it was able to!
This commit is contained in:
Michael Bolin
2025-12-11 14:46:00 -08:00
committed by GitHub
parent bacbe871c8
commit e0d7ac51d3
11 changed files with 58 additions and 54 deletions

View File

@@ -8,7 +8,12 @@ use tempfile::TempDir;
#[test]
fn execpolicy_check_matches_expected_json() -> Result<(), Box<dyn std::error::Error>> {
let codex_home = TempDir::new()?;
let policy_path = codex_home.path().join("policy.codexpolicy");
let policy_path = codex_home.path().join("rules").join("policy.rules");
fs::create_dir_all(
policy_path
.parent()
.expect("policy path should have a parent"),
)?;
fs::write(
&policy_path,
r#"
@@ -24,7 +29,7 @@ prefix_rule(
.args([
"execpolicy",
"check",
"--policy",
"--rules",
policy_path
.to_str()
.expect("policy path should be valid UTF-8"),

View File

@@ -30,9 +30,9 @@ const FORBIDDEN_REASON: &str = "execpolicy forbids this command";
const PROMPT_CONFLICT_REASON: &str =
"execpolicy requires approval for this command, but AskForApproval is set to Never";
const PROMPT_REASON: &str = "execpolicy requires approval for this command";
const POLICY_DIR_NAME: &str = "policy";
const POLICY_EXTENSION: &str = "codexpolicy";
const DEFAULT_POLICY_FILE: &str = "default.codexpolicy";
const RULES_DIR_NAME: &str = "rules";
const RULE_EXTENSION: &str = "rules";
const DEFAULT_POLICY_FILE: &str = "default.rules";
fn is_policy_match(rule_match: &RuleMatch) -> bool {
match rule_match {
@@ -92,7 +92,7 @@ pub(crate) async fn load_exec_policy_for_features(
}
pub async fn load_exec_policy(codex_home: &Path) -> Result<Policy, ExecPolicyError> {
let policy_dir = codex_home.join(POLICY_DIR_NAME);
let policy_dir = codex_home.join(RULES_DIR_NAME);
let policy_paths = collect_policy_files(&policy_dir).await?;
let mut parser = PolicyParser::new();
@@ -124,7 +124,7 @@ pub async fn load_exec_policy(codex_home: &Path) -> Result<Policy, ExecPolicyErr
}
pub(crate) fn default_policy_path(codex_home: &Path) -> PathBuf {
codex_home.join(POLICY_DIR_NAME).join(DEFAULT_POLICY_FILE)
codex_home.join(RULES_DIR_NAME).join(DEFAULT_POLICY_FILE)
}
pub(crate) async fn append_execpolicy_amendment_and_update(
@@ -304,7 +304,7 @@ async fn collect_policy_files(dir: &Path) -> Result<Vec<PathBuf>, ExecPolicyErro
if path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext == POLICY_EXTENSION)
.is_some_and(|ext| ext == RULE_EXTENSION)
&& file_type.is_file()
{
policy_paths.push(path);
@@ -349,14 +349,14 @@ mod tests {
},
policy.check_multiple(commands.iter(), &|_| Decision::Allow)
);
assert!(!temp_dir.path().join(POLICY_DIR_NAME).exists());
assert!(!temp_dir.path().join(RULES_DIR_NAME).exists());
}
#[tokio::test]
async fn collect_policy_files_returns_empty_when_dir_missing() {
let temp_dir = tempdir().expect("create temp dir");
let policy_dir = temp_dir.path().join(POLICY_DIR_NAME);
let policy_dir = temp_dir.path().join(RULES_DIR_NAME);
let files = collect_policy_files(&policy_dir)
.await
.expect("collect policy files");
@@ -367,10 +367,10 @@ mod tests {
#[tokio::test]
async fn loads_policies_from_policy_subdirectory() {
let temp_dir = tempdir().expect("create temp dir");
let policy_dir = temp_dir.path().join(POLICY_DIR_NAME);
let policy_dir = temp_dir.path().join(RULES_DIR_NAME);
fs::create_dir_all(&policy_dir).expect("create policy dir");
fs::write(
policy_dir.join("deny.codexpolicy"),
policy_dir.join("deny.rules"),
r#"prefix_rule(pattern=["rm"], decision="forbidden")"#,
)
.expect("write policy file");
@@ -395,7 +395,7 @@ mod tests {
async fn ignores_policies_outside_policy_dir() {
let temp_dir = tempdir().expect("create temp dir");
fs::write(
temp_dir.path().join("root.codexpolicy"),
temp_dir.path().join("root.rules"),
r#"prefix_rule(pattern=["ls"], decision="prompt")"#,
)
.expect("write policy file");
@@ -423,7 +423,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
"#;
let mut parser = PolicyParser::new();
parser
.parse("test.codexpolicy", policy_src)
.parse("test.rules", policy_src)
.expect("parse policy");
let policy = Arc::new(RwLock::new(parser.build()));
@@ -456,7 +456,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
let policy_src = r#"prefix_rule(pattern=["rm"], decision="prompt")"#;
let mut parser = PolicyParser::new();
parser
.parse("test.codexpolicy", policy_src)
.parse("test.rules", policy_src)
.expect("parse policy");
let policy = Arc::new(RwLock::new(parser.build()));
let command = vec!["rm".to_string()];
@@ -485,7 +485,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
let policy_src = r#"prefix_rule(pattern=["rm"], decision="prompt")"#;
let mut parser = PolicyParser::new();
parser
.parse("test.codexpolicy", policy_src)
.parse("test.rules", policy_src)
.expect("parse policy");
let policy = Arc::new(RwLock::new(parser.build()));
let command = vec!["rm".to_string()];
@@ -537,7 +537,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
let policy_src = r#"prefix_rule(pattern=["apple"], decision="allow")"#;
let mut parser = PolicyParser::new();
parser
.parse("test.codexpolicy", policy_src)
.parse("test.rules", policy_src)
.expect("parse policy");
let policy = Arc::new(RwLock::new(parser.build()));
let command = vec![
@@ -668,7 +668,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
let policy_src = r#"prefix_rule(pattern=["rm"], decision="prompt")"#;
let mut parser = PolicyParser::new();
parser
.parse("test.codexpolicy", policy_src)
.parse("test.rules", policy_src)
.expect("parse policy");
let policy = Arc::new(RwLock::new(parser.build()));
let command = vec!["rm".to_string()];
@@ -726,7 +726,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
let policy_src = r#"prefix_rule(pattern=["cat"], decision="allow")"#;
let mut parser = PolicyParser::new();
parser
.parse("test.codexpolicy", policy_src)
.parse("test.rules", policy_src)
.expect("parse policy");
let policy = Arc::new(RwLock::new(parser.build()));
@@ -783,7 +783,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
let policy_src = r#"prefix_rule(pattern=["echo"], decision="allow")"#;
let mut parser = PolicyParser::new();
parser
.parse("test.codexpolicy", policy_src)
.parse("test.rules", policy_src)
.expect("parse policy");
let policy = Arc::new(RwLock::new(parser.build()));
let command = vec!["echo".to_string(), "safe".to_string()];

View File

@@ -1633,7 +1633,7 @@ async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts
.await?;
wait_for_completion(&test).await;
let policy_path = test.home.path().join("policy").join("default.codexpolicy");
let policy_path = test.home.path().join("rules").join("default.rules");
let policy_contents = fs::read_to_string(&policy_path)?;
assert!(
policy_contents

View File

@@ -27,7 +27,7 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> {
}
let mut builder = test_codex().with_config(|config| {
let policy_path = config.codex_home.join("policy").join("policy.codexpolicy");
let policy_path = config.codex_home.join("rules").join("policy.rules");
fs::create_dir_all(
policy_path
.parent()

View File

@@ -72,9 +72,9 @@ pub async fn write_default_execpolicy<P>(policy: &str, codex_home: P) -> anyhow:
where
P: AsRef<Path>,
{
let policy_dir = codex_home.as_ref().join("policy");
let policy_dir = codex_home.as_ref().join("rules");
tokio::fs::create_dir_all(&policy_dir).await?;
tokio::fs::write(policy_dir.join("default.codexpolicy"), policy).await?;
tokio::fs::write(policy_dir.join("default.rules"), policy).await?;
Ok(())
}

View File

@@ -16,10 +16,10 @@ use tempfile::TempDir;
#[tokio::test(flavor = "current_thread")]
async fn list_tools() -> Result<()> {
let codex_home = TempDir::new()?;
let policy_dir = codex_home.path().join("policy");
let policy_dir = codex_home.path().join("rules");
fs::create_dir_all(&policy_dir)?;
fs::write(
policy_dir.join("default.codexpolicy"),
policy_dir.join("default.rules"),
r#"prefix_rule(pattern=["ls"], decision="prompt")"#,
)?;
let dotslash_cache_temp_dir = TempDir::new()?;

View File

@@ -20,14 +20,14 @@ prefix_rule(
```
## CLI
- From the Codex CLI, run `codex execpolicy check` subcommand with one or more policy files (for example `src/default.codexpolicy`) to check a command:
- From the Codex CLI, run `codex execpolicy check` subcommand with one or more policy files (for example `src/default.rules`) to check a command:
```bash
codex execpolicy check --policy path/to/policy.codexpolicy git status
codex execpolicy check --rules path/to/policy.rules git status
```
- Pass multiple `--policy` flags to merge rules, evaluated in the order provided, and use `--pretty` for formatted JSON.
- Pass multiple `--rules` flags to merge rules, evaluated in the order provided, and use `--pretty` for formatted JSON.
- You can also run the standalone dev binary directly during development:
```bash
cargo run -p codex-execpolicy -- check --policy path/to/policy.codexpolicy git status
cargo run -p codex-execpolicy -- check --rules path/to/policy.rules git status
```
- Example outcomes:
- Match: `{"matchedRules":[{...}],"decision":"allow"}`

View File

@@ -154,7 +154,7 @@ mod tests {
#[test]
fn appends_rule_and_creates_directories() {
let tmp = tempdir().expect("create temp dir");
let policy_path = tmp.path().join("policy").join("default.codexpolicy");
let policy_path = tmp.path().join("rules").join("default.rules");
blocking_append_allow_prefix_rule(
&policy_path,
@@ -162,8 +162,7 @@ mod tests {
)
.expect("append rule");
let contents =
std::fs::read_to_string(&policy_path).expect("default.codexpolicy should exist");
let contents = std::fs::read_to_string(&policy_path).expect("default.rules should exist");
assert_eq!(
contents,
r#"prefix_rule(pattern=["echo", "Hello, world!"], decision="allow")
@@ -174,7 +173,7 @@ mod tests {
#[test]
fn appends_rule_without_duplicate_newline() {
let tmp = tempdir().expect("create temp dir");
let policy_path = tmp.path().join("policy").join("default.codexpolicy");
let policy_path = tmp.path().join("rules").join("default.rules");
std::fs::create_dir_all(policy_path.parent().unwrap()).expect("create policy dir");
std::fs::write(
&policy_path,
@@ -201,7 +200,7 @@ prefix_rule(pattern=["echo", "Hello, world!"], decision="allow")
#[test]
fn inserts_newline_when_missing_before_append() {
let tmp = tempdir().expect("create temp dir");
let policy_path = tmp.path().join("policy").join("default.codexpolicy");
let policy_path = tmp.path().join("rules").join("default.rules");
std::fs::create_dir_all(policy_path.parent().unwrap()).expect("create policy dir");
std::fs::write(
&policy_path,

View File

@@ -14,9 +14,9 @@ use crate::RuleMatch;
/// Arguments for evaluating a command against one or more execpolicy files.
#[derive(Debug, Parser, Clone)]
pub struct ExecPolicyCheckCommand {
/// Paths to execpolicy files to evaluate (repeatable).
#[arg(short = 'p', long = "policy", value_name = "PATH", required = true)]
pub policies: Vec<PathBuf>,
/// Paths to execpolicy rule files to evaluate (repeatable).
#[arg(short = 'r', long = "rules", value_name = "PATH", required = true)]
pub rules: Vec<PathBuf>,
/// Pretty-print the JSON output.
#[arg(long)]
@@ -35,7 +35,7 @@ pub struct ExecPolicyCheckCommand {
impl ExecPolicyCheckCommand {
/// Load the policies for this command, evaluate the command, and render JSON output.
pub fn run(&self) -> Result<()> {
let policy = load_policies(&self.policies)?;
let policy = load_policies(&self.rules)?;
let matched_rules = policy.matches_for_command(&self.command, None);
let json = format_matches_json(&matched_rules, self.pretty)?;

View File

@@ -54,7 +54,7 @@ prefix_rule(
)
"#;
let mut parser = PolicyParser::new();
parser.parse("test.codexpolicy", policy_src)?;
parser.parse("test.rules", policy_src)?;
let policy = parser.build();
let cmd = tokens(&["git", "status"]);
let evaluation = policy.check(&cmd, &allow_all);
@@ -129,8 +129,8 @@ prefix_rule(
)
"#;
let mut parser = PolicyParser::new();
parser.parse("first.codexpolicy", first_policy)?;
parser.parse("second.codexpolicy", second_policy)?;
parser.parse("first.rules", first_policy)?;
parser.parse("second.rules", second_policy)?;
let policy = parser.build();
let git_rules = rule_snapshots(policy.rules().get_vec("git").context("missing git rules")?);
@@ -194,7 +194,7 @@ prefix_rule(
)
"#;
let mut parser = PolicyParser::new();
parser.parse("test.codexpolicy", policy_src)?;
parser.parse("test.rules", policy_src)?;
let policy = parser.build();
let bash_rules = rule_snapshots(
@@ -259,7 +259,7 @@ prefix_rule(
)
"#;
let mut parser = PolicyParser::new();
parser.parse("test.codexpolicy", policy_src)?;
parser.parse("test.rules", policy_src)?;
let policy = parser.build();
let rules = rule_snapshots(policy.rules().get_vec("npm").context("missing npm rules")?);
@@ -323,7 +323,7 @@ prefix_rule(
)
"#;
let mut parser = PolicyParser::new();
parser.parse("test.codexpolicy", policy_src)?;
parser.parse("test.rules", policy_src)?;
let policy = parser.build();
let match_eval = policy.check(&tokens(&["git", "status"]), &allow_all);
assert_eq!(
@@ -367,7 +367,7 @@ prefix_rule(
)
"#;
let mut parser = PolicyParser::new();
parser.parse("test.codexpolicy", policy_src)?;
parser.parse("test.rules", policy_src)?;
let policy = parser.build();
let commit = policy.check(&tokens(&["git", "commit", "-m", "hi"]), &allow_all);
@@ -403,7 +403,7 @@ prefix_rule(
)
"#;
let mut parser = PolicyParser::new();
parser.parse("test.codexpolicy", policy_src)?;
parser.parse("test.rules", policy_src)?;
let policy = parser.build();
let commands = vec![

View File

@@ -1,6 +1,6 @@
# Execpolicy quickstart
Codex can enforce your own rules-based execution policy before it runs shell commands. Policies live in `.codexpolicy` files under `~/.codex/policy`.
Codex can enforce your own rules-based execution policy before it runs shell commands. Policies live in `.rules` files under `~/.codex/rules`.
## How to create and edit rules
@@ -12,12 +12,12 @@ Codex CLI will present the option to whitelist commands when a command causes a
Whitelisted commands will no longer require your permission to run in current and subsequent sessions.
Under the hood, when you approve and whitelist a command, codex will edit `~/.codex/policy/default.codexpolicy`.
Under the hood, when you approve and whitelist a command, codex will edit `~/.codex/rules/default.rules`.
### Editing `.codexpolicy` files
### Editing `.rules` files
1. Create a policy directory: `mkdir -p ~/.codex/policy`.
2. Add one or more `.codexpolicy` files in that folder. Codex automatically loads every `.codexpolicy` file in there on startup.
1. Create a policy directory: `mkdir -p ~/.codex/rules`.
2. Add one or more `.rules` files in that folder. Codex automatically loads every `.rules` file in there on startup.
3. Write `prefix_rule` entries to describe the commands you want to allow, prompt, or block:
```starlark
@@ -40,10 +40,10 @@ In this example rule, if Codex wants to run commands with the prefix `git push`
Use the `codex execpolicy check` subcommand to preview decisions before you save a rule (see the [`codex-execpolicy` README](../codex-rs/execpolicy/README.md) for syntax details):
```shell
codex execpolicy check --policy ~/.codex/policy/default.codexpolicy git push origin main
codex execpolicy check --rules ~/.codex/rules/default.rules git push origin main
```
Pass multiple `--policy` flags to test how several files combine, and use `--pretty` for formatted JSON output. See the [`codex-rs/execpolicy` README](../codex-rs/execpolicy/README.md) for a more detailed walkthrough of the available syntax.
Pass multiple `--rules` flags to test how several files combine, and use `--pretty` for formatted JSON output. See the [`codex-rs/execpolicy` README](../codex-rs/execpolicy/README.md) for a more detailed walkthrough of the available syntax.
Example output when a rule matches: