Command Classification (execpolicy)
The execpolicy crate is codex's command classification engine: given an argv, it decides whether to allow, prompt, or forbid. Rules are written in Starlark in the rules/ directory; at runtime PolicyParser compiles them into a Policy, and Policy::check runs before each command. core's ExecPolicyManager layers approval policy and heuristic fallback on top.
Responsibilities
- Express each rule's verdict with a three-valued
Decisionenum:codex-rs/execpolicy/src/decision.rs:9-27 - Use
PrefixRule/NetworkRulefor prefix matching and network domain matching:codex-rs/execpolicy/src/rule.rs:40-115 - The Starlark parser compiles policy files into a
Policy:codex-rs/execpolicy/src/parser.rs:38-83 - Core-side
ExecPolicyManagermerges the policy result with approval policy and dangerous-command heuristics:codex-rs/core/src/exec_policy.rs:312-410 - For commands that no rule matches, derive a default Decision from profile and command source:
codex-rs/core/src/exec_policy.rs:728-760
Design motivation
Why not regex or glob? Because the command structure itself is argv, and prefix matching is more precise than string regex — and easier to write back. After the user clicks "always allow", blocking_append_allow_prefix_rule directly appends a line prefix_rule(pattern=["npm", "run"], decision="allow") to the policy file; the same argv hits the rule next time. PrefixPattern supports two token types, Single and Alts ([a|b]), covering exactly the "prefix + choose-one-of-two args" case.
Why Starlark instead of TOML? Because policy needs conditional logic ("allow only if host is in some list"), needs functions (prefix_rule, network_rule), and TOML cannot express that. Starlark is a Python subset; policy files are highly readable and can be safely evaluated by the Rust starlark crate. PolicyParser binds the AST and PolicyBuilder together with RefCell; after parsing, build produces an immutable Policy in one shot.
Decision is three-valued rather than two-valued to distinguish "explicitly forbidden" from "needs approval". forbidden blocks outright, prompt triggers the user-approval UI, allow skips approval. When multiple rules hit, the max wins — Allow < Prompt < Forbidden; the strictest verdict wins.
Key files
codex-rs/execpolicy/src/lib.rs:1-33— crate entry, re-exporting all public API.codex-rs/execpolicy/src/decision.rs:9-27—Decisionenum andparse.codex-rs/execpolicy/src/policy.rs:28-67—Policystruct andrules_by_program/network_rules/host_executables.codex-rs/execpolicy/src/policy.rs:188-251—check/check_multiple_with_optionsentry.codex-rs/execpolicy/src/policy.rs:351-375—Evaluationaggregates multi-rule results;from_matchespicks the strictest verdict.codex-rs/execpolicy/src/rule.rs:40-60—PrefixPattern::matches_prefixdoes prefix matching.codex-rs/execpolicy/src/parser.rs:38-83—PolicyParserparses Starlark policy files.codex-rs/execpolicy/src/amend.rs:65-125— appends new rules to the policy file at runtime.codex-rs/core/src/command_canonicalization.rs:14-38— canonicalizesbash -lc-wrapped commands before matching.
Decision is three-valued rather than two-valued to distinguish "explicitly forbidden" from "needs approval".
pub enum Decision {
/// Command may run without further approval.
Allow,
/// Request explicit user approval; rejected outright when running with `approval_policy="never"`.
Prompt,
/// Command is blocked without further consideration.
Forbidden,
}Prefix matching is more precise than string regex and easier to write back — after the user clicks "always allow", blocking_append_allow_prefix_rule directly composes a Starlark call and appends it to the policy file.
pub fn blocking_append_allow_prefix_rule(
policy_path: &Path,
prefix: &[String],
) -> Result<(), AmendError> {
if prefix.is_empty() {
return Err(AmendError::EmptyPrefix);
}
let tokens = prefix
.iter()
.map(serde_json::to_string)
.collect::<Result<Vec<_>, _>>()
.map_err(|source| AmendError::SerializePrefix { source })?;
let pattern = format!("[{}]", tokens.join(", "));
let rule = format!(r#"prefix_rule(pattern={pattern}, decision="allow")"#);
append_rule_line(policy_path, &rule)
}core also layers heuristics on top of the policy verdict: when no rule matches, render_decision_for_unmatched_command looks at is_known_safe_command, profile_has_managed_filesystem_restrictions, and so on to derive the default Decision.
let evaluation = exec_policy.check_multiple_with_options(
commands.iter(),
&exec_policy_fallback,
&match_options,
);
// ...
match evaluation.decision {
Decision::Forbidden => ExecApprovalRequirement::Forbidden { reason: /* ... */ },
Decision::Prompt => { /* 走审批流程 */ }
Decision::Allow => /* 直接放行 */,
}Data flow
Boundaries and failures
parse_shell_lc_plain_commandsonly handles simple shell; heredoc / redirect / pipe goes throughused_complex_parsing=true, and no auto-derived rule fix is allowed:codex-rs/core/src/exec_policy.rs:325-353- On multi-rule conflict,
from_matchestakesmax(), i.e.Allow < Prompt < Forbidden; the strictest verdict wins:codex-rs/execpolicy/src/policy.rs:357-374 - A
network_rule's host cannot contain a scheme or path;normalize_network_rule_hostrejects forms likehttps://example.com/foo:codex-rs/execpolicy/src/rule.rs:156-189 - The heuristic fallback
render_decision_for_unmatched_commandlooks atwindows_sandbox_leveland the profile's managed FS restrictions — when the Windows sandbox is off, even a strictly-written profile takes the conservative path:codex-rs/core/src/exec_policy.rs:751-757 canonicalize_command_for_approvalprefixesbash -lc '...'-style commands with__codex_shell_script__, to avoid wrapper-path differences causing rules to miss:codex-rs/core/src/command_canonicalization.rs:21-35
Summary
execpolicy is the core of codex's command classification: Starlark policy compiles into Policy; PrefixRule and NetworkRule do precise matching; unmatched commands go through core's heuristic fallback. The three-valued Decision lets "allow / approve / forbid" be expressed in one semantics, and the strictest verdict wins on conflict. Understanding this chain is key to debugging "why does this command need approval" and "why didn't the user-saved rule take effect". The actual execution lands in command execution exec-server and cross-platform sandbox.