From e0d7ac51d30df4fb937b18fa8df680304093afd0 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 11 Dec 2025 14:46:00 -0800 Subject: [PATCH] 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! --- codex-rs/cli/tests/execpolicy.rs | 9 +++-- codex-rs/core/src/exec_policy.rs | 36 +++++++++---------- codex-rs/core/tests/suite/approvals.rs | 2 +- codex-rs/core/tests/suite/exec_policy.rs | 2 +- codex-rs/exec-server/tests/common/lib.rs | 4 +-- .../exec-server/tests/suite/list_tools.rs | 4 +-- codex-rs/execpolicy/README.md | 8 ++--- codex-rs/execpolicy/src/amend.rs | 9 +++-- codex-rs/execpolicy/src/execpolicycheck.rs | 8 ++--- codex-rs/execpolicy/tests/basic.rs | 16 ++++----- docs/execpolicy.md | 14 ++++---- 11 files changed, 58 insertions(+), 54 deletions(-) diff --git a/codex-rs/cli/tests/execpolicy.rs b/codex-rs/cli/tests/execpolicy.rs index 4610b95874..241a873d59 100644 --- a/codex-rs/cli/tests/execpolicy.rs +++ b/codex-rs/cli/tests/execpolicy.rs @@ -8,7 +8,12 @@ use tempfile::TempDir; #[test] fn execpolicy_check_matches_expected_json() -> Result<(), Box> { 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"), diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index 6de2967c76..8917610ffc 100644 --- a/codex-rs/core/src/exec_policy.rs +++ b/codex-rs/core/src/exec_policy.rs @@ -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 { - 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 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, 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()]; diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index 10a510af42..d1b1a515df 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -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 diff --git a/codex-rs/core/tests/suite/exec_policy.rs b/codex-rs/core/tests/suite/exec_policy.rs index b65b2994ee..470478ad75 100644 --- a/codex-rs/core/tests/suite/exec_policy.rs +++ b/codex-rs/core/tests/suite/exec_policy.rs @@ -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() diff --git a/codex-rs/exec-server/tests/common/lib.rs b/codex-rs/exec-server/tests/common/lib.rs index 0c29528220..f4a70f5b1f 100644 --- a/codex-rs/exec-server/tests/common/lib.rs +++ b/codex-rs/exec-server/tests/common/lib.rs @@ -72,9 +72,9 @@ pub async fn write_default_execpolicy

(policy: &str, codex_home: P) -> anyhow: where P: AsRef, { - 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(()) } diff --git a/codex-rs/exec-server/tests/suite/list_tools.rs b/codex-rs/exec-server/tests/suite/list_tools.rs index 2f3d412df7..4a28dec5b3 100644 --- a/codex-rs/exec-server/tests/suite/list_tools.rs +++ b/codex-rs/exec-server/tests/suite/list_tools.rs @@ -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()?; diff --git a/codex-rs/execpolicy/README.md b/codex-rs/execpolicy/README.md index 1ddb67252f..288a46dcbc 100644 --- a/codex-rs/execpolicy/README.md +++ b/codex-rs/execpolicy/README.md @@ -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"}` diff --git a/codex-rs/execpolicy/src/amend.rs b/codex-rs/execpolicy/src/amend.rs index 36a02cebf9..9114d3a64f 100644 --- a/codex-rs/execpolicy/src/amend.rs +++ b/codex-rs/execpolicy/src/amend.rs @@ -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, diff --git a/codex-rs/execpolicy/src/execpolicycheck.rs b/codex-rs/execpolicy/src/execpolicycheck.rs index 939ed8590b..f191f4b12e 100644 --- a/codex-rs/execpolicy/src/execpolicycheck.rs +++ b/codex-rs/execpolicy/src/execpolicycheck.rs @@ -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, + /// Paths to execpolicy rule files to evaluate (repeatable). + #[arg(short = 'r', long = "rules", value_name = "PATH", required = true)] + pub rules: Vec, /// 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)?; diff --git a/codex-rs/execpolicy/tests/basic.rs b/codex-rs/execpolicy/tests/basic.rs index 3ea33604ae..7ae5e6e213 100644 --- a/codex-rs/execpolicy/tests/basic.rs +++ b/codex-rs/execpolicy/tests/basic.rs @@ -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![ diff --git a/docs/execpolicy.md b/docs/execpolicy.md index e60e1cec6c..ecc79f33d2 100644 --- a/docs/execpolicy.md +++ b/docs/execpolicy.md @@ -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: