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!
1. Skills load once in core at session start; the cached outcome is
reused across core and surfaced to TUI via SessionConfigured.
2. TUI detects explicit skill selections, and core injects the matching
SKILL.md content into the turn when a selected skill is present.
- Make Config.model optional and centralize default-selection logic in
ModelsManager, including a default_model helper (with
codex-auto-balanced when available) so sessions now carry an explicit
chosen model separate from the base config.
- Resolve `model` once in `core` and `tui` from config. Then store the
state of it on other structs.
- Move refreshing models to be before resolving the default model
helpful in the future if we want more granularity for requesting
escalated permissions:
e.g when running in readonly sandbox, model can request to escalate to a
sandbox that allows writes
# External (non-OpenAI) Pull Request Requirements
Before opening this Pull Request, please read the dedicated
"Contributing" markdown file or your PR may be closed:
https://github.com/openai/codex/blob/main/docs/contributing.md
If your PR conforms to our contribution guidelines, replace this text
with a detailed and high quality description of your changes.
Include a link to a bug report or enhancement request.
Issue #7661 revealed that users are confused by deprecation warnings
like:
> `tools.web_search` is deprecated. Use `web_search_request` instead.
This message misleadingly suggests renaming the config key from
`web_search` to `web_search_request`, when the actual required change is
to **move and rename the configuration from the `[tools]` section to the
`[features]` section**.
This PR clarifies the warning messages and documentation to make it
clear that deprecated `[tools]` configurations should be moved to
`[features]`. Changes made:
- Updated deprecation warning format in `codex-rs/core/src/codex.rs:520`
to include `[features].` prefix
- Updated corresponding test expectations in
`codex-rs/core/tests/suite/deprecation_notice.rs:39`
- Improved documentation in `docs/config.md` to clarify upfront that
`[tools]` options are deprecated in favor of `[features]`
# External (non-OpenAI) Pull Request Requirements
Before opening this Pull Request, please read the dedicated
"Contributing" markdown file or your PR may be closed:
https://github.com/openai/codex/blob/main/docs/contributing.md
If your PR conforms to our contribution guidelines, replace this text
with a detailed and high quality description of your changes.
Include a link to a bug report or enhancement request.
## Summary
This PR is heavily based on #4017, which contains the core logic for the
fix. To reduce the risk, we are first introducing it only on windows. We
can then expand to wsl / other environments as needed, and then tackle
net new files.
## Testing
- [x] added unit tests in apply-patch
- [x] add integration tests to apply_patch_cli.rs
---------
Co-authored-by: Chase Naples <Cnaples79@gmail.com>
- This PR wires `with_remote_overrides` and make the
`construct_model_families` an async function
- Moves getting model family a level above to keep the function `sync`
- Updates the tests to local, offline, and `sync` helper for model
families
- Introduce `with_remote_overrides` and update
`refresh_available_models`
- Put `auth_manager` instead of `auth_mode` on `models_manager`
- Remove `ShellType` and `ReasoningLevel` to use already existing
structs
**Change**: Seatbelt now allows file-ioctl on /dev/ttys[0-9]+ even
without the sandbox extension so pre-created PTYs remain interactive
(Python REPL, shells).
**Risk**: A seatbelted process that already holds a PTY fd (including
one it shouldn’t) could issue tty ioctls like TIOCSTI or termios changes
on that fd. This doesn’t allow opening new PTYs or reading/writing them;
it only broadens ioctl capability on existing fds.
**Why acceptable**: We already hand the child its PTY for interactive
use; restoring ioctls is required for isatty() and prompts to work. The
attack requires being given or inheriting a sensitive PTY fd; by design
we don’t hand untrusted processes other users’ PTYs (we don't hand them
any PTYs actually), so the practical exposure is limited to the PTY
intentionally allocated for the session.
**Validation**:
Running
```
start a python interpreter and keep it running
```
Followed by:
* `calculate 1+1 using it` -> works as expected
* `Use this Python session to run the command just fix in
/Users/jif/code/codex/codex-rs` -> does not work as expected
## Refactor of the `execpolicy` crate
To illustrate why we need this refactor, consider an agent attempting to
run `apple | rm -rf ./`. Suppose `apple` is allowed by `execpolicy`.
Before this PR, `execpolicy` would consider `apple` and `pear` and only
render one rule match: `Allow`. We would skip any heuristics checks on
`rm -rf ./` and immediately approve `apple | rm -rf ./` to run.
To fix this, we now thread a `fallback` evaluation function into
`execpolicy` that runs when no `execpolicy` rules match a given command.
In our example, we would run `fallback` on `rm -rf ./` and prevent
`apple | rm -rf ./` from being run without approval.
this PR enables TUI to approve commands and add their prefixes to an
allowlist:
<img width="708" height="605" alt="Screenshot 2025-11-21 at 4 18 07 PM"
src="https://github.com/user-attachments/assets/56a19893-4553-4770-a881-becf79eeda32"
/>
note: we only show the option to whitelist the command when
1) command is not multi-part (e.g `git add -A && git commit -m 'hello
world'`)
2) command is not already matched by an existing rule
This PR moves `ModelsFamily` to `openai_models`. It also propagates
`ModelsManager` to session services and use it to drive model family. We
also make `derive_default_model_family` private because it's a step
towards what we want: one place that gives model configuration.
This is a second step at having one source of truth for models
information and config: `ModelsManager`.
Next steps would be to remove `ModelsFamily` from config. That's massive
because it's being used in 41 occasions mostly pre launching `codex`.
Also, we need to make `find_family_for_model` private. It's also big
because it's being used in 21 occasions ~ all tests.
- Introduce `openai_models` in `/core`
- Move `PRESETS` under it
- Move `ModelPreset`, `ModelUpgrade`, `ReasoningEffortPreset`,
`ReasoningEffortPreset`, and `ReasoningEffortPreset` to `protocol`
- Introduce `Op::ListModels` and `EventMsg::AvailableModels`
Next steps:
- migrate `app-server` and `tui` to use the introduced Operation
If an image can't be read by the API, it will poison the entire history,
preventing any new turn on the conversation.
This detect such cases and replace the image by a placeholder
# Unified Exec Shell Selection on Windows
## Problem
reference issue #7466
The `unified_exec` handler currently deserializes model-provided tool
calls into the `ExecCommandArgs` struct:
```rust
#[derive(Debug, Deserialize)]
struct ExecCommandArgs {
cmd: String,
#[serde(default)]
workdir: Option<String>,
#[serde(default = "default_shell")]
shell: String,
#[serde(default = "default_login")]
login: bool,
#[serde(default = "default_exec_yield_time_ms")]
yield_time_ms: u64,
#[serde(default)]
max_output_tokens: Option<usize>,
#[serde(default)]
with_escalated_permissions: Option<bool>,
#[serde(default)]
justification: Option<String>,
}
```
The `shell` field uses a hard-coded default:
```rust
fn default_shell() -> String {
"/bin/bash".to_string()
}
```
When the model returns a tool call JSON that only contains `cmd` (which
is the common case), Serde fills in `shell` with this default value.
Later, `get_command` uses that value as if it were a model-provided
shell path:
```rust
fn get_command(args: &ExecCommandArgs) -> Vec<String> {
let shell = get_shell_by_model_provided_path(&PathBuf::from(args.shell.clone()));
shell.derive_exec_args(&args.cmd, args.login)
}
```
On Unix, this usually resolves to `/bin/bash` and works as expected.
However, on Windows this behavior is problematic:
- The hard-coded `"/bin/bash"` is not a valid Windows path.
- `get_shell_by_model_provided_path` treats this as a model-specified
shell, and tries to resolve it (e.g. via `which::which("bash")`), which
may or may not exist and may not behave as intended.
- In practice, this leads to commands being executed under a non-default
or non-existent shell on Windows (for example, WSL bash), instead of the
expected Windows PowerShell or `cmd.exe`.
The core of the issue is that **"model did not specify `shell`" is
currently interpreted as "the model explicitly requested `/bin/bash`"**,
which is both Unix-specific and wrong on Windows.
## Proposed Solution
Instead of hard-coding `"/bin/bash"` into `ExecCommandArgs`, we should
distinguish between:
1. **The model explicitly specifying a shell**, e.g.:
```json
{
"cmd": "echo hello",
"shell": "pwsh"
}
```
In this case, we *do* want to respect the model’s choice and use
`get_shell_by_model_provided_path`.
2. **The model omitting the `shell` field entirely**, e.g.:
```json
{
"cmd": "echo hello"
}
```
In this case, we should *not* assume `/bin/bash`. Instead, we should use
`default_user_shell()` and let the platform decide.
To express this distinction, we can:
1. Change `shell` to be optional in `ExecCommandArgs`:
```rust
#[derive(Debug, Deserialize)]
struct ExecCommandArgs {
cmd: String,
#[serde(default)]
workdir: Option<String>,
#[serde(default)]
shell: Option<String>,
#[serde(default = "default_login")]
login: bool,
#[serde(default = "default_exec_yield_time_ms")]
yield_time_ms: u64,
#[serde(default)]
max_output_tokens: Option<usize>,
#[serde(default)]
with_escalated_permissions: Option<bool>,
#[serde(default)]
justification: Option<String>,
}
```
Here, the absence of `shell` in the JSON is represented as `shell:
None`, rather than a hard-coded string value.
This change prototypes support for Skills with the CLI. This is an
**experimental** feature for internal testing.
---------
Co-authored-by: Gav Verma <gverma@openai.com>