# Split command parsing/safety out of codex-core into new codex-command (#11361)

`codex-core` had accumulated command parsing and command safety logic
(`bash`, `powershell`, `parse_command`, and `command_safety`) that is
logically cohesive but orthogonal to most core session/runtime logic.
Keeping this code in `codex-core` made the crate increasingly monolithic
and raised iteration cost for unrelated core changes.

This change extracts that surface into a dedicated crate,
`codex-command`, while preserving existing `codex_core::...` call sites
via re-exports.

## Why this refactor

During analysis, command parsing/safety stood out as a good first split
because it has:

- a clear domain boundary (shell parsing + safety classification)
- relatively self-contained dependencies (notably `tree-sitter` /
`tree-sitter-bash`)
- a meaningful standalone test surface (`134` tests moved with the
crate)
- many downstream uses that benefit from independent compilation and
caching

The practical problem was build latency from a large `codex-core`
compile/test graph. Clean-build timings before and after this split
showed measurable wins:

- `cargo check -p codex-core`: `57.08s` -> `53.54s` (~`6.2%` faster)
- `cargo test -p codex-core --no-run`: `2m39.9s` -> `2m20s` (~`12.4%`
faster)
- `codex-core lib` compile unit: `57.18s` -> `49.67s` (~`13.1%` faster)
- `codex-core lib(test)` compile unit: `60.87s` -> `53.21s` (~`12.6%`
faster)

This gives a concrete reduction in core build overhead without changing
behavior.

## What changed

### New crate

- Added `codex-rs/command` as workspace crate `codex-command`.
- Added:
  - `command/src/lib.rs`
  - `command/src/bash.rs`
  - `command/src/powershell.rs`
  - `command/src/parse_command.rs`
  - `command/src/command_safety/*`
  - `command/src/shell_detect.rs`
  - `command/BUILD.bazel`

### Code moved out of `codex-core`

- Moved modules from `core/src` into `command/src`:
  - `bash.rs`
  - `powershell.rs`
  - `parse_command.rs`
  - `command_safety/*`

### Dependency graph updates

- Added workspace member/dependency entries for `codex-command` in
`codex-rs/Cargo.toml`.
- Added `codex-command` dependency to `codex-rs/core/Cargo.toml`.
- Removed `tree-sitter` and `tree-sitter-bash` from `codex-core` direct
deps (now owned by `codex-command`).

### API compatibility for callers

To avoid immediate downstream churn, `codex-core` now re-exports the
moved modules/functions:

- `codex_command::bash`
- `codex_command::powershell`
- `codex_command::parse_command`
- `codex_command::is_safe_command`
- `codex_command::is_dangerous_command`

This keeps existing `codex_core::...` paths working while enabling
gradual migration to direct `codex-command` usage.

### Internal decoupling detail

- Added `command::shell_detect` so moved `bash`/`powershell` logic no
longer depends on core shell internals.
- Adjusted PowerShell helper visibility in `codex-command` for existing
core test usage (`UTF8` prefix helper + executable discovery functions).

## Validation

- `just fmt`
- `just fix -p codex-command -p codex-core`
- `cargo test -p codex-command` (`134` passed)
- `cargo test -p codex-core --no-run`
- `cargo test -p codex-core shell_command_handler`

## Notes / follow-up

This commit intentionally prioritizes boundary extraction and
compatibility. A follow-up can migrate downstream crates to depend
directly on `codex-command` (instead of through `codex-core` re-exports)
to realize additional incremental build wins.
This commit is contained in:
Michael Bolin
2026-02-10 14:43:16 -08:00
committed by GitHub
parent 3626399811
commit d8f9bb65e2
18 changed files with 117 additions and 24 deletions
+21 -2
View File
@@ -1623,6 +1623,26 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "codex-command"
version = "0.0.0"
dependencies = [
"anyhow",
"base64 0.22.1",
"codex-protocol",
"codex-utils-absolute-path",
"once_cell",
"pretty_assertions",
"regex",
"serde",
"serde_json",
"shlex",
"tree-sitter",
"tree-sitter-bash",
"url",
"which",
]
[[package]]
name = "codex-common"
version = "0.0.0"
@@ -1660,6 +1680,7 @@ dependencies = [
"codex-arg0",
"codex-async-utils",
"codex-client",
"codex-command",
"codex-core",
"codex-execpolicy",
"codex-file-search",
@@ -1733,8 +1754,6 @@ dependencies = [
"tracing",
"tracing-subscriber",
"tracing-test",
"tree-sitter",
"tree-sitter-bash",
"url",
"uuid",
"walkdir",
+2
View File
@@ -16,6 +16,7 @@ members = [
"cloud-tasks-client",
"cli",
"common",
"command",
"core",
"hooks",
"secrets",
@@ -82,6 +83,7 @@ codex-chatgpt = { path = "chatgpt" }
codex-cli = { path = "cli"}
codex-client = { path = "codex-client" }
codex-common = { path = "common" }
codex-command = { path = "command" }
codex-core = { path = "core" }
codex-hooks = { path = "hooks" }
codex-secrets = { path = "secrets" }
+7
View File
@@ -0,0 +1,7 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "command",
crate_name = "codex_command",
compile_data = ["src/command_safety/powershell_parser.ps1"],
)
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "codex-command"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
base64 = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
once_cell = { workspace = true }
regex = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
shlex = { workspace = true }
tree-sitter = { workspace = true }
tree-sitter-bash = { workspace = true }
url = { workspace = true }
which = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
pretty_assertions = { workspace = true }
@@ -5,8 +5,8 @@ use tree_sitter::Parser;
use tree_sitter::Tree;
use tree_sitter_bash::LANGUAGE as BASH;
use crate::shell::ShellType;
use crate::shell::detect_shell_type;
use crate::shell_detect::ShellType;
use crate::shell_detect::detect_shell_type;
/// Parse the provided bash source using tree-sitter-bash, returning a Tree on
/// success or None if parsing failed.
+11
View File
@@ -0,0 +1,11 @@
//! Command parsing and safety utilities shared across Codex crates.
mod shell_detect;
pub mod bash;
pub mod command_safety;
pub mod parse_command;
pub mod powershell;
pub use command_safety::is_dangerous_command;
pub use command_safety::is_safe_command;
@@ -1,18 +1,16 @@
use std::path::PathBuf;
#[cfg(any(windows, test))]
use codex_utils_absolute_path::AbsolutePathBuf;
use crate::shell::ShellType;
use crate::shell::detect_shell_type;
use crate::shell_detect::ShellType;
use crate::shell_detect::detect_shell_type;
const POWERSHELL_FLAGS: &[&str] = &["-nologo", "-noprofile", "-command", "-c"];
/// Prefixed command for powershell shell calls to force UTF-8 console output.
pub(crate) const UTF8_OUTPUT_PREFIX: &str =
"[Console]::OutputEncoding=[System.Text.Encoding]::UTF8;\n";
pub const UTF8_OUTPUT_PREFIX: &str = "[Console]::OutputEncoding=[System.Text.Encoding]::UTF8;\n";
pub(crate) fn prefix_powershell_script_with_utf8(command: &[String]) -> Vec<String> {
pub fn prefix_powershell_script_with_utf8(command: &[String]) -> Vec<String> {
let Some((_, script)) = extract_powershell_command(command) else {
return command.to_vec();
};
@@ -84,8 +82,7 @@ pub(crate) fn try_find_powershellish_executable_blocking() -> Option<AbsolutePat
}
/// This function attempts to find a powershell.exe executable on the system.
#[cfg(any(windows, test))]
pub(crate) fn try_find_powershell_executable_blocking() -> Option<AbsolutePathBuf> {
pub fn try_find_powershell_executable_blocking() -> Option<AbsolutePathBuf> {
try_find_powershellish_executable_in_path(&["powershell.exe"])
}
@@ -99,8 +96,7 @@ pub(crate) fn try_find_powershell_executable_blocking() -> Option<AbsolutePathBu
/// pwsh.exe must be installed separately by the user. And even when the user
/// has installed pwsh.exe, it may not be available in the system PATH, in which
/// case we attempt to locate it via other means.
#[cfg(any(windows, test))]
pub(crate) fn try_find_pwsh_executable_blocking() -> Option<AbsolutePathBuf> {
pub fn try_find_pwsh_executable_blocking() -> Option<AbsolutePathBuf> {
if let Some(ps_home) = std::process::Command::new("cmd")
.args(["/C", "pwsh", "-NoProfile", "-Command", "$PSHOME"])
.output()
@@ -126,7 +122,6 @@ pub(crate) fn try_find_pwsh_executable_blocking() -> Option<AbsolutePathBuf> {
try_find_powershellish_executable_in_path(&["pwsh.exe"])
}
#[cfg(any(windows, test))]
fn try_find_powershellish_executable_in_path(candidates: &[&str]) -> Option<AbsolutePathBuf> {
for candidate in candidates {
let Ok(resolved_path) = which::which(candidate) else {
@@ -147,7 +142,6 @@ fn try_find_powershellish_executable_in_path(candidates: &[&str]) -> Option<Abso
None
}
#[cfg(any(windows, test))]
fn is_powershellish_executable_available(powershell_or_pwsh_exe: &std::path::Path) -> bool {
// This test works for both powershell.exe and pwsh.exe.
std::process::Command::new(powershell_or_pwsh_exe)
+32
View File
@@ -0,0 +1,32 @@
use std::path::Path;
use std::path::PathBuf;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum ShellType {
Zsh,
Bash,
PowerShell,
Sh,
Cmd,
}
pub(crate) fn detect_shell_type(shell_path: &PathBuf) -> Option<ShellType> {
match shell_path.as_os_str().to_str() {
Some("zsh") => Some(ShellType::Zsh),
Some("sh") => Some(ShellType::Sh),
Some("cmd") => Some(ShellType::Cmd),
Some("bash") => Some(ShellType::Bash),
Some("pwsh") => Some(ShellType::PowerShell),
Some("powershell") => Some(ShellType::PowerShell),
_ => {
let shell_name = shell_path.file_stem();
if let Some(shell_name) = shell_name {
let shell_name_path = Path::new(shell_name);
if shell_name_path != Path::new(shell_path) {
return detect_shell_type(&shell_name_path.to_path_buf());
}
}
None
}
}
}
+1 -2
View File
@@ -33,6 +33,7 @@ codex-app-server-protocol = { workspace = true }
codex-apply-patch = { workspace = true }
codex-async-utils = { workspace = true }
codex-client = { workspace = true }
codex-command = { workspace = true }
codex-execpolicy = { workspace = true }
codex-file-search = { workspace = true }
codex-git = { workspace = true }
@@ -106,8 +107,6 @@ tokio-tungstenite = { workspace = true }
toml = { workspace = true }
toml_edit = { workspace = true }
tracing = { workspace = true, features = ["log"] }
tree-sitter = { workspace = true }
tree-sitter-bash = { workspace = true }
url = { workspace = true }
uuid = { workspace = true, features = ["serde", "v4", "v5"] }
which = { workspace = true }
+5 -6
View File
@@ -10,7 +10,6 @@ pub mod api_bridge;
mod apply_patch;
mod apps;
pub mod auth;
pub mod bash;
mod client;
mod client_common;
pub mod codex;
@@ -22,7 +21,6 @@ pub use codex_thread::ThreadConfigSnapshot;
mod agent;
mod codex_delegate;
mod command_canonicalization;
mod command_safety;
pub mod config;
pub mod config_loader;
pub mod connectors;
@@ -52,10 +50,8 @@ mod memories;
mod mentions;
mod message_history;
mod model_provider_info;
pub mod parse_command;
pub mod path_utils;
pub mod personality_migration;
pub mod powershell;
mod proposed_plan_parser;
mod sandbox_tags;
pub mod sandboxing;
@@ -133,11 +129,14 @@ mod state;
mod tasks;
mod user_shell_command;
pub mod util;
pub use codex_command::bash;
pub use codex_command::is_dangerous_command;
pub use codex_command::is_safe_command;
pub use codex_command::parse_command;
pub use codex_command::powershell;
pub use apply_patch::CODEX_APPLY_PATCH_ARG1;
pub use client::X_CODEX_TURN_METADATA_HEADER;
pub use command_safety::is_dangerous_command;
pub use command_safety::is_safe_command;
pub use exec_policy::ExecPolicyError;
pub use exec_policy::check_execpolicy_for_warnings;
pub use exec_policy::load_exec_policy;