These rules are mandatory. All code in this repo must follow these conventions exactly. They are derived from the original OpenAI team's patterns and enforced by tooling (clippy, rustfmt, prettier, eslint). Full evidence and file references:
docs/pattern/CODING_CONVENTIONS.md.
| Directory | Purpose |
|---|---|
codex-rs/ |
Primary codebase — Rust implementation (67+ crates) |
codex-cli/ |
npm wrapper — thin JS launcher resolving platform-specific Rust binaries |
sdk/ |
Client SDKs (Python + TypeScript) for programmatic access |
shell-tool-mcp/ |
MCP server exposing shell tool capabilities |
scripts/ |
Repo-wide utility scripts (release, install) |
docs/ |
Documentation (contributing, install, config) |
tools/ |
Developer tooling (argument-comment linting via Dylint) |
just codex # Run Orbit Code from source
just test # Run Rust tests (nextest)
just fmt # Format Rust code
just fix # Run clippy fixes
just fix -p <crate> # Clippy fix scoped to one crate
just write-config-schema # Regenerate config JSON schema
just write-app-server-schema # Regenerate app-server protocol schemas
just bazel-lock-update # Update MODULE.bazel.lock after dep changes
just bazel-lock-check # Verify lockfile is in sync
just argument-comment-lint # Run /*param*/ comment lintBuild systems: Cargo (local dev) + Bazel (CI/release). TypeScript: pnpm + tsup. Task runner: just (working dir = codex-rs/).
- Never add or modify code related to
CODEX_SANDBOX_NETWORK_DISABLED_ENV_VARorCODEX_SANDBOX_ENV_VAR. These are set by the sandbox runtime and existing code was authored with this in mind.CODEX_SANDBOX_NETWORK_DISABLED=1is set whenever theshelltool runs in sandbox.CODEX_SANDBOX=seatbeltis set on child processes spawned under macOS Seatbelt (/usr/bin/sandbox-exec). Existing checks for these env vars are intentional early-exit guards — do not remove or modify them. - Never mutate process environment in tests. Prefer passing environment-derived flags or dependencies from above.
- If you add
include_str!,include_bytes!, orsqlx::migrate!, update the crate'sBUILD.bazel(compile_data,build_script_data, or test data) or Bazel will fail even when Cargo passes. - Install any commands the repo relies on (e.g.,
just,rg,cargo-insta,cargo-nextest) if they aren't already available before running instructions.
- Declare modules private by default with
mod foo;. Re-export only needed types viapub use foo::Type;in lib.rs. - Use
pub modonly for major subsystem modules that form the crate's public API surface. Protocol crates (protocol,app-server-protocol) export everything publicly. - Use a single
.rsfile for focused modules. Use a subdirectory withmod.rswhen a module has 3+ sub-modules. - Place integration tests in
tests/all.rs->tests/suite/mod.rs->tests/suite/*.rs. Usetests/common/for shared test utilities as a library crate. Never create multiple top-level test binaries. - Target modules under 500 LoC excluding tests. At ~800 LoC, add new functionality in a new module instead of extending the existing file. Do not create single-use helper methods. This applies especially to high-touch files:
tui/src/app.rs,tui/src/bottom_pane/chat_composer.rs,tui/src/bottom_pane/footer.rs,tui/src/chatwidget.rs,tui/src/bottom_pane/mod.rs, and similarly central orchestration modules. - When extracting code from a large module, move the related tests and module/type docs toward the new implementation so invariants stay close to the code that owns them.
- Define error types with
#[derive(Debug, Error)]from thiserror. Every variant must have an#[error(...)]attribute. Use#[error(transparent)]for wrapped errors. - Define
pub type Result<T> = std::result::Result<T, YourError>;alongside each crate-level error type. - Prefer
#[from]for direct error wrapping. Use manualFromimpls only when conversion requires custom logic. - Add domain-specific query methods to error types (e.g.,
is_retryable(),to_protocol_error()). Use exhaustive matches, not wildcards. - Never use
unwrap()orexpect()in library code. Use?or explicit error handling. Tests may useexpect("descriptive message").
- Tokio is the sole async runtime.
#[tokio::test]for async tests,tokio::spawnfor background tasks,Handle::current()only when you need async work in Drop impls. - Channel selection:
broadcastfor fan-out notifications,oneshotfor single request/response,async_channelfor MPMC. - Shared state:
tokio::sync::Mutexfor async locking,tokio::sync::RwLockfor read-heavy state. Always wrap inArc<>. - Use
CancellationTokenwith.child_token()for hierarchical, coordinated shutdown of subsystems. - Use
JoinSetfor parallel Tokio tasks. UseFuturesUnorderedfor stream-based concurrent futures. - Retries: exponential backoff (
2^(attempt-1)) with +/-10% jitter viarandom_range(0.9..1.1). Use saturating arithmetic. Only retry transient errors (429, 5xx, connection failures).
- Derive sets by type category:
- Config:
Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema - Protocol: add
TS - App-server v2: add
TS+ExperimentalApiwhere needed
- Config:
rename_allby context:kebab-casefor config TOML,snake_casefor protocol,camelCasefor app-server v2. Exception: config RPC payloads usesnake_caseto mirror TOML keys.- NEVER use
skip_serializing_ifon v2Option<T>payload fields — use#[ts(optional = nullable)]instead. For v2 booleans defaulting to false:#[serde(default, skip_serializing_if = "std::ops::Not::not")] pub field: bool. - Always add
#[ts(export_to = "v2/")]on v2 types. Keep#[serde(rename)]and#[ts(rename)]aligned. For tagged unions:#[serde(tag = "type")]and#[ts(tag = "type")].
- Add
Send + Syncbounds to traits used behindArc<dyn Trait>or across thread boundaries. Add+ 'staticwhen the trait object must outlive its scope. - Use
#[async_trait]for any trait with async methods. Place it directly above the trait definition. - Provide default implementations for optional trait methods. Use no-op defaults for lifecycle hooks (abort, cleanup). Document the default behavior.
- Use
pub(crate)for crate-internal types,pub(super)for parent-module-only types. Private by default; selective re-exports viapub use.
- One import per
usestatement (enforced byimports_granularity = "Item"in rustfmt.toml). Runjust fmtto enforce ordering automatically. - Package names:
orbit-code-<name>(hyphens). Library names:orbit_code_<name>(underscores). Binary names:orbit-codeororbit-code-<tool>(hyphens). - App-server types:
*Paramsfor requests,*Responsefor responses,*Notificationfor notifications. RPC methods:<singular-resource>/<camelCaseMethod>(e.g.,thread/start,fs/readFile).
- Avoid bool or ambiguous
Optionparameters that force callers to writefoo(false)orbar(None). Prefer enums, named methods, or newtypes to keep callsites self-documenting. - When you cannot change the API and have opaque literal arguments (booleans,
None, numbers), use/*param_name*/comments before them. The name must exactly match the callee's parameter. String/char literals are exempt unless the comment adds real clarity. - Use
#[derive(Debug, Parser)]with#[clap(flatten)]for shared option groups and#[clap(visible_alias)]for subcommand aliases. - Prefer plain
StringIDs at API boundaries. Do UUID parsing/conversion internally. Timestamps: integer Unix seconds (i64) named*_at. - When making exhaustive
matchstatements, avoid wildcard arms. Prefer listing all variants so the compiler catches new additions.
- Add
//!module docs to every module describing its purpose and key types. - Document all public items with
///. Use [TypeName] syntax for cross-references. Skip docs for trivial getters and self-evident enum variants. - Use
//inline comments only for non-obvious logic, invariants, or "why" explanations. Never restate what the code does. - When a change adds or modifies an API, update documentation in the
docs/folder if applicable. At minimum updateapp-server/README.mdfor app-server changes.
- Use
pretty_assertions::assert_eq!in every test module. Compare entire objects withassert_eq!, not individual fields. - Add
instasnapshot tests for any UI-affecting change. Snapshot workflow:- Run tests:
cargo test -p codex-tui - Check pending:
cargo insta pending-snapshots -p codex-tui - Preview:
cargo insta show -p codex-tui path/to/file.snap.new - Accept:
cargo insta accept -p codex-tui - Install if missing:
cargo install cargo-insta
- Run tests:
- Use
wiremock::MockServerand helpers fromcore_test_support::responsesfor HTTP mocking. Prefermount_sse_onceovermount_sse_once_matchormount_sse_sequence. Allmount_sse*helpers return aResponseMock— hold onto it to assert against outbound requests. UseResponseMock::single_request()for single-POST tests,ResponseMock::requests()to inspect all captured requests.ResponsesRequestexposes:body_json,input,function_call_output,custom_tool_call_output,call_output,header,path,query_param. Build SSE payloads withev_*constructors andsse(...). Typical pattern:let mock = responses::mount_sse_once(&server, responses::sse(vec![ responses::ev_response_created("resp-1"), responses::ev_function_call(call_id, "shell", &serde_json::to_string(&args)?), responses::ev_completed("resp-1"), ])).await; codex.submit(Op::UserTurn { ... }).await?; let request = mock.single_request(); // assert using request.function_call_output(call_id) or request.body_json()
- Use
TestCodexBuilderfluent API for integration test setup. Chain.with_config(),.with_model(),.with_auth(),.with_home(),.with_pre_build_hook(). - Use
wait_for_event(codex, predicate)for async event assertions. Prefer it overwait_for_event_with_timeout. - Use
codex_utils_cargo_bin::cargo_bin()for binary resolution (works with Cargo and Bazel). Usefind_resource!instead ofenv!("CARGO_MANIFEST_DIR"). - Use
#[ctor]intests/common/lib.rsfor process-startup initialization (deterministic IDs, insta workspace root). - Avoid boilerplate tests that only assert experimental field markers for individual request fields in
common.rs— rely on schema generation/tests and behavioral coverage instead.
- 33 clippy lints are denied workspace-wide. Key rules enforced:
unwrap_used,expect_used— no panics in library code (allowed in tests)uninlined_format_args— alwaysformat!("{var}")notformat!("{}", var)redundant_closure_for_method_calls— use method references over closurescollapsible_if— always collapse nested ifs- All
manual_*rules — use idiomatic Rust - All
needless_*rules — no unnecessary borrows, collects, late inits
- Disallowed ratatui methods (enforced in clippy.toml):
Color::Rgb,Color::Indexed,.white(),.black(),.yellow(). Use ANSI colors only. - Large-error-threshold: 256 bytes. Box large payloads if an error variant exceeds this.
- Crates with TUI output must add
#![deny(clippy::print_stdout, clippy::print_stderr)]at the top of lib.rs.
- Use Stylize trait helpers:
"text".red(),"text".dim(),"text".bold(),"text".cyan(). Chain:url.cyan().underlined(). UseSpan::styledonly for runtime-computed styles. - Basic spans:
"text".into(). Lines:vec![span1, span2].into(). UseLine::from(vec![...])only when the target type isn't obvious. Prefer the form that stays on one line after rustfmt. - Color palette from
tui/styles.md: Headers = bold. Secondary = dim. Selection/tips = cyan. Success = green. Errors = red. Branding = magenta. Never use blue, yellow, white, black, Rgb, or Indexed. - Text wrapping:
adaptive_wrap_lines()for content with URLs,word_wrap_lines()for plain text,textwrap::wrap()for raw strings. Useprefix_linesfromline_utilsfor indented multi-line output. For indented wrapped lines, useinitial_indent/subsequent_indentoptions fromRtOptionsrather than writing custom logic. - Mirror all
tui/changes intui_app_server/unless there is a documented reason not to. - Don't refactor between equivalent forms (
Span::styled<->set_style,Line::from<->.into()) without a clear readability or functional gain. Follow file-local conventions. Do not introduce type annotations solely to satisfy.into().
- Config types must derive
JsonSchema. Runjust write-config-schemaafter anyConfigTomlchange. - Run
just write-app-server-schema(and--experimentalwhen needed) after API shape changes. Validate withcargo test -p codex-app-server-protocol. - Add all dependencies to
[workspace.dependencies]in rootCargo.toml. Per-crate:{ workspace = true }with crate-specific feature overrides only. - After any dependency change: run
just bazel-lock-updatethenjust bazel-lock-check. Include the lockfile update in the same change. - Standard dev-dependencies:
pretty_assertions(diffs),tempfile(temp dirs),wiremock(HTTP mocking),insta(snapshots).
- ESM-first:
"type": "module"in package.json. Useimport/exportsyntax exclusively. - Always use
node:prefix for built-in imports:import from "node:fs",import from "node:path". - Target ES2022 with ESNext modules. Enable
strict: trueandnoUncheckedIndexedAccess: true. export typefor type-only re-exports from index.ts.export classfor concrete implementations.- Prefix unused parameters with
_. - tsup for bundling (ESM output for SDK, CJS for MCP servers). Jest with ts-jest for testing.
- Prettier with project config (
trailingComma: "all"). ESLint flat config with typescript-eslint.
- Hatchling build system. Pydantic v2 models.
src/layout. - pytest with
-q. ruff for linting. Auto-generate models from JSON schema.
- Run
just fmtafter every Rust change. Do not ask for approval — just run it. - Run
just fix -p <crate>before finalizing large changes. Scope with-pto avoid slow workspace-wide builds. Only runjust fixwithout-pif you changed shared crates. - Test the changed crate first:
cargo test -p <crate>. Project-specific or individual tests can be run without asking the user. Ask the user before running the complete test suite (just test). Run full suite only if core, common, or protocol crates changed. Avoid--all-featuresfor routine runs — it expands the build matrix and increasestarget/disk usage. - Do not re-run tests after running
just fixorjust fmt. - All active API development happens in app-server v2. Do not add new API surface area to v1.
- For experimental API surface: use
#[experimental("method/or/field")], deriveExperimentalApiwhen field-level gating is needed, and useinspect_params: trueincommon.rswhen only some fields of a method are experimental.
- Payloads:
*Params(request),*Response(response),*Notification(notification) - Wire format:
camelCasevia#[serde(rename_all = "camelCase")] - Optional fields in
*Params:#[ts(optional = nullable)]. Do not use#[ts(optional = nullable)]outside*Params. - Optional collections in
*Params: useOption<Vec<...>>+#[ts(optional = nullable)]. Never use#[serde(default)]for optional collections, and do not useskip_serializing_ifon v2 payload fields. - Boolean defaults-to-false:
#[serde(default, skip_serializing_if = "std::ops::Not::not")] pub field: bool - No-params exception: client->server requests with no params may use
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()> - Cursor pagination for list methods: request
cursor: Option<String>+limit: Option<u32>, responsedata: Vec<...>+next_cursor: Option<String> - TypeScript export:
#[ts(export_to = "v2/")]on all v2 types - Validate with
cargo test -p codex-app-server-protocol - Key files:
app-server-protocol/src/protocol/common.rs,app-server-protocol/src/protocol/v2.rs,app-server/README.md