The Rust core crate is codexcw (crates.io), in crates/codexcw.
cargo add codexcw
# the streaming recipes also use:
cargo add tokio --features macros,rt-multi-thread
cargo add tokio-streamRunners drive Codex (the default agent) or Claude Code; the selected agent's
executable must be on PATH and authenticated — codex new enough to support
codex exec --json, claude new enough to support
--output-format stream-json (see the Claude agent section below). Codex
defaults are automation-friendly: read-only sandbox, approval never,
ephemeral sessions, color off, git-check skipped.
The core is async (tokio). Use .await inside an async runtime, or wrap a call
in Runtime::block_on for synchronous callers.
use codexcw::{Request, Runner};
// Async (the idiomatic form).
#[tokio::main]
async fn main() -> Result<(), codexcw::Error> {
let runner = Runner::new();
let result = runner.run(Request::new("diga oi")).await?;
println!("{}", result.final_message);
Ok(())
}// Blocking: drive the same async call from sync code.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let rt = tokio::runtime::Runtime::new()?;
let runner = codexcw::Runner::new();
let result = rt.block_on(runner.run(codexcw::Request::new("diga oi")))?;
println!("{}", result.final_message);
Ok(())
}Every recipe below shows the .await form; wrap it in rt.block_on(...) for the
blocking equivalent.
use codexcw::{EventPayload, ItemKind, Request, Runner};
use tokio_stream::StreamExt;
let runner = Runner::new();
let mut session = runner.start(Request::new("resuma este repo")).await?;
let mut events = session.events();
while let Some(event) = events.next().await {
if let EventPayload::ItemCompleted(item) = &event.payload {
if item.kind == ItemKind::AgentMessage {
println!("{}", item.text);
}
}
}
let result = session.wait().await?;
println!("tokens: {}", result.usage.total_tokens);RunOptions::with_handler registers an async callback for every event. Return
Err to cancel the run (surfaced as Error::Handler).
use codexcw::{handler, EventPayload, ItemKind, Request, RunOptions, Runner};
let runner = Runner::new();
let opts = RunOptions::with_handler(handler(|event| async move {
if let EventPayload::ItemCompleted(item) = &event.payload {
if item.kind == ItemKind::CommandExecution {
println!("$ {}", item.command);
}
}
Ok(())
}));
let result = runner.run_opts(Request::new("trabalhe"), opts).await?;
let _ = result;// A handler that aborts on the first command execution.
let opts = RunOptions::with_handler(handler(|event| async move {
match &event.payload {
EventPayload::ItemStarted(item) if item.kind == ItemKind::CommandExecution => {
Err("stop".to_string())
}
_ => Ok(()),
}
}));
match runner.run_opts(Request::new("..."), opts).await {
Err(codexcw::Error::Handler(msg)) => println!("cancelled by handler: {msg}"),
other => { let _ = other; }
}let runner = Runner::new();
let first = runner.run(Request::new("crie um arquivo TODO.md")).await?;
let thread_id = first.thread_id.clone();
let second = runner
.run(Request {
prompt: "agora adicione 3 itens ao TODO.md".to_string(),
resume_id: Some(thread_id),
..Default::default()
})
.await?;
println!("{}", second.final_message);// Resume the most recent thread:
runner.run(Request { prompt: "continue".into(), resume_last: true, ..Default::default() }).await?;
// ResumeAll disables Codex's cwd filtering while resuming:
runner.run(Request {
prompt: "continue".into(),
resume_id: Some(thread_id),
resume_all: true,
..Default::default()
}).await?;Resume runs reject
dir,add_dirs, andprofilewithError::InvalidRequest.
use codexcw::{Request, SandboxMode};
// Read-only is the default. Let Codex write inside the workspace:
runner.run(Request::new("refatore o módulo foo").sandbox(SandboxMode::WorkspaceWrite)).await?;
// Remove sandbox filesystem restrictions:
runner.run(Request::new("...").sandbox(SandboxMode::DangerFullAccess)).await?;use codexcw::{ApprovalPolicy, Request, SandboxMode};
// Defaults to ApprovalPolicy::Never. The safer interactive middle ground:
let req = Request::new("...")
.sandbox(SandboxMode::WorkspaceWrite)
.approval(ApprovalPolicy::OnRequest);
runner.run(req).await?;Danger.
dangerously_bypass_sandboxruns Codex with--dangerously-bypass-approvals-and-sandbox: no sandbox, no approval prompts. Only use this in a disposable, fully-trusted environment.
runner.run(Request {
prompt: "...".to_string(),
dangerously_bypass_sandbox: true,
..Default::default()
}).await?;
// Run enabled hooks without persisted trust:
runner.run(Request {
prompt: "...".to_string(),
dangerously_bypass_hooks: true,
..Default::default()
}).await?;use codexcw::{ManyOptions, Request, Runner};
use tokio_stream::StreamExt;
let runner = Runner::new();
let mut group = runner
.run_many(
vec![
Request::new("review package A"),
Request::new("review package B"),
Request::new("review package C"),
],
ManyOptions { max_concurrent: 2, ..Default::default() },
)
.await;
let mut events = group.events();
while let Some(run_event) = events.next().await {
println!("[{}] {}", run_event.index, run_event.event.kind);
}
match group.wait().await {
Ok(results) => {
for r in results {
if let Some(report) = r.result {
println!("[{}] {}", r.index, report.final_message);
}
}
}
Err(group_error) => {
for r in group_error.results {
if let Some(err) = r.error {
println!("[{}] failed: {err}", r.index);
}
}
}
}use codexcw::{ConfigOverride, Request};
runner.run(Request {
prompt: "...".to_string(),
config: vec![
ConfigOverride::new("model_reasoning_effort", "\"high\""),
ConfigOverride::new("tools.web_search", "true"),
],
..Default::default()
}).await?;Codex Fast mode uses the priority service tier.
runner.run(Request {
prompt: "...".to_string(),
config: vec![ConfigOverride::new("service_tier", "\"priority\"")],
..Default::default()
}).await?;let schema = br#"{"type":"object","properties":{"summary":{"type":"string"}},"required":["summary"]}"#;
let result = runner.run(Request {
prompt: "resuma o repo como JSON".to_string(),
output_schema: Some(schema.to_vec()),
output_last_message_path: Some("out.json".to_string()),
..Default::default()
}).await?;
println!("{}", result.final_message);runner.run(Request {
prompt: "...".to_string(),
dir: Some("/work/project".to_string()),
add_dirs: vec!["/work/shared".to_string(), "/work/vendor".to_string()],
..Default::default()
}).await?;runner.run(Request {
prompt: "...".to_string(),
model: Some("o3".to_string()),
profile: Some("work".to_string()),
..Default::default()
}).await?;The runner also wraps Claude Code's non-interactive mode
(claude -p --output-format stream-json). Select it on the builder; the
claude executable must be on PATH and authenticated. Events are normalized
into the same [Event] model — thread.started carries the Claude session
id, tool calls become item.started/item.completed pairs, and the final
result maps to turn.completed — with raw always keeping the original
Claude JSON line.
use codexcw::{claude_model, permission_mode, Agent, Request, Runner};
let runner = Runner::builder().agent(Agent::Claude).build();
let result = runner
.run(Request {
prompt: "crie um arquivo TODO.md".to_string(),
model: Some(claude_model::HAIKU.to_string()), // HAIKU, SONNET, or OPUS
permission_mode: Some(permission_mode::ACCEPT_EDITS.to_string()),
..Default::default()
})
.await?;
println!("total tokens: {}", result.usage.total_tokens);
println!("cache creation: {}", result.usage.cache_creation_input_tokens);
println!("cost: ${}", result.usage.total_cost_usd);
for (model, usage) in &result.usage.model_usage {
println!(
"{model}: {} input, {} output",
usage.input_tokens, usage.output_tokens
);
}// Tool filters and resume work per request:
let _ = runner
.run(Request {
prompt: "rode os testes".to_string(),
model: Some(claude_model::SONNET.to_string()),
allowed_tools: vec!["Bash(cargo test *)".to_string(), "Read".to_string()],
disallowed_tools: vec!["WebSearch".to_string()],
..Default::default()
})
.await;
let first = runner
.run(Request {
prompt: "lembre disto".to_string(),
persistent: true,
..Default::default()
})
.await?;
let _ = runner
.run(Request {
prompt: "continue".to_string(),
resume_id: Some(first.thread_id.clone()), // or resume_last: true
persistent: true,
..Default::default()
})
.await;Claude runs support dir (applied as the process working directory),
add_dirs, output_schema/output_schema_path (passed as --json-schema),
and dangerously_bypass_sandbox (passed as
--dangerously-skip-permissions). permission_mode, allowed_tools, and
disallowed_tools are claude-only; codex-only fields (sandbox, approval,
profile, config, images, feature flags) return
Error::InvalidRequest on a claude runner.
The permission-mode constants are ACCEPT_EDITS, AUTO,
BYPASS_PERMISSIONS, MANUAL, DONT_ASK, and PLAN.
// Prompt via stdin only:
runner.run(Request { stdin: Some(b"diga oi".to_vec()), ..Default::default() }).await?;
// Prompt plus extra stdin context (wrapped in <stdin> markers):
runner.run(Request::new("resuma o diff abaixo").stdin(large_diff.into_bytes())).await?;let runner = Runner::builder()
.executable("/opt/codex/bin/codex")
.env("CODEX_HOME", "/tmp/codex-home")
.build();get_account_usage reads account limits and credits through codex app-server.
It accepts an executable override and child-process environment. CODEX_HOME
defaults to ~/.codex when it is not set. timeout bounds each JSON-RPC
request; None uses the 10-second default.
use std::time::Duration;
use codexcw::{get_account_usage, AccountUsageRequest};
let usage = get_account_usage(AccountUsageRequest {
env: vec![("CODEX_HOME".to_string(), "/tmp/codex-home".to_string())],
timeout: Some(Duration::from_secs(5)),
..Default::default()
}).await?;
if let Some(account) = &usage.account {
println!("account: {}", account.email);
}
if let Some(primary) = &usage.rate_limits.primary {
println!("primary used: {}", primary.used_percent);
}
if let Some(token_usage) = &usage.token_usage {
println!("lifetime tokens: {:?}", token_usage.summary.lifetime_tokens);
}account and token_usage are None when codex answers those reads with a
JSON-RPC error; transport errors and timeouts fail the whole call.
Claude account usage comes from /usage. The typed windows contain the label,
used percentage, and Claude's reset description; report and raw preserve
the complete human-readable report and JSON result line.
use codexcw::{get_claude_account_usage, ClaudeAccountUsageRequest};
let usage = get_claude_account_usage(ClaudeAccountUsageRequest::default()).await?;
for window in &usage.windows {
println!(
"{}: {}% used, resets {}",
window.label, window.used_percent, window.resets_at
);
}use codexcw::{Error, Request, Runner};
match runner.run(Request::new("...")).await {
Ok(result) => println!("{}", result.final_message),
Err(Error::Exit { code, stderr, .. }) => println!("agent exited {code}: {stderr}"),
Err(Error::Codex { message, .. }) => println!("codex reported: {message}"),
Err(Error::Claude { message, .. }) => println!("claude reported: {message}"),
Err(Error::Decode { line, .. }) => println!("bad JSONL on line {line}"),
Err(Error::PromptRequired) => println!("prompt or stdin is required"),
Err(other) => println!("error: {other}"),
}let session = runner.start(Request::new("...")).await?;
session.cancel(); // stops the child process; wait() then reports the outcome
let _ = session.wait().await;See the README for the cross-language overview and AGENTS.md for the project guide.