The PyPI package is codexcw (in bindings/python), a native extension backed
by the Rust core.
pip install codexcwRunners 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 package ships both: a synchronous API in codexcw and an asyncio API in
codexcw.aio with the same shapes. Every recipe below is shown in both.
# Synchronous
from codexcw import Runner, Request
runner = Runner()
result = runner.run(Request(prompt="diga oi"))
print(result.final_message)# Asynchronous (codexcw.aio)
import asyncio
from codexcw import Request
from codexcw.aio import Runner
async def main():
runner = Runner()
result = await runner.run(Request(prompt="diga oi"))
print(result.final_message)
asyncio.run(main())# Sync
from codexcw import Runner, Request
runner = Runner()
session = runner.start(Request(prompt="resuma este repo"))
for event in session.events():
if event.type == "item.completed" and event.item.type == "agent_message":
print(event.item.text)
result = session.wait()
print("tokens:", result.usage.total_tokens)# Async
from codexcw import Request
from codexcw.aio import Runner
runner = Runner()
session = await runner.start(Request(prompt="resuma este repo"))
async for event in session.events():
if event.type == "item.completed" and event.item.type == "agent_message":
print(event.item.text)
result = await session.wait()
print("tokens:", result.usage.total_tokens)on_event runs for every event. Raising cancels the run.
# Sync
def on_event(event):
if event.type == "item.completed" and event.item.type == "command_execution":
print("$", event.item.command)
runner.run(Request(prompt="trabalhe"), on_event)# Async — the callback may be a plain function or a coroutine.
async def on_event(event):
if event.type == "item.completed" and event.item.type == "command_execution":
print("$", event.item.command)
await runner.run(Request(prompt="trabalhe"), on_event)# A callback that aborts on the first command execution.
from codexcw import CodexcwError
def stop_on_command(event):
if event.type == "item.started" and event.item.type == "command_execution":
raise RuntimeError("stop")
try:
runner.run(Request(prompt="..."), stop_on_command)
except RuntimeError as err:
print("cancelled:", err)# Sync
first = runner.run(Request(prompt="crie um arquivo TODO.md"))
thread_id = first.thread_id
second = runner.run(Request(prompt="agora adicione 3 itens ao TODO.md", resume_id=thread_id))
print(second.final_message)# Async
first = await runner.run(Request(prompt="crie um arquivo TODO.md"))
second = await runner.run(Request(prompt="continue", resume_id=first.thread_id))# Resume the most recent thread, or disable cwd filtering with resume_all:
runner.run(Request(prompt="continue", resume_last=True))
runner.run(Request(prompt="continue", resume_id=thread_id, resume_all=True))Resume runs reject
dir,add_dirs, andprofile— raised as aCodexcwErrorwithkind == "invalidRequest".
# Read-only is the default. Let Codex write inside the workspace:
runner.run(Request(prompt="refatore o pacote foo", sandbox="workspace-write"))
await runner.run(Request(prompt="refatore o pacote foo", sandbox="workspace-write"))
# Remove sandbox filesystem restrictions:
runner.run(Request(prompt="...", sandbox="danger-full-access"))# Defaults to "never". The safer interactive middle ground:
req = Request(prompt="...", sandbox="workspace-write", approval="on-request")
runner.run(req) # sync
await runner.run(req) # asyncDanger.
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="...", dangerously_bypass_sandbox=True))
await runner.run(Request(prompt="...", dangerously_bypass_sandbox=True))
# Run enabled hooks without persisted trust:
runner.run(Request(prompt="...", dangerously_bypass_hooks=True))# Sync
group = runner.run_many(
[Request(prompt="review A"), Request(prompt="review B"), Request(prompt="review C")],
max_concurrent=2,
)
for run_event in group.events():
print(f"[{run_event.index}] {run_event.event.type}")
for r in group.wait():
if r.error is not None:
print(f"[{r.index}] failed:", r.error.kind, r.error)
else:
print(f"[{r.index}] {r.result.final_message}")# Async
group = await runner.run_many(
[Request(prompt="review A"), Request(prompt="review B"), Request(prompt="review C")],
max_concurrent=2,
)
async for run_event in group.events():
print(f"[{run_event.index}] {run_event.event.type}")
results = await group.wait()from codexcw import ConfigOverride
req = Request(prompt="...", config=[
ConfigOverride(key="model_reasoning_effort", value='"high"'),
ConfigOverride(key="tools.web_search", value="true"),
])
runner.run(req)
await runner.run(req)Codex Fast mode uses the priority service tier.
req = Request(
prompt="...",
config=[ConfigOverride(key="service_tier", value='"priority"')],
)
runner.run(req)
await runner.run(req)import json
schema = json.dumps({
"type": "object",
"properties": {"summary": {"type": "string"}},
"required": ["summary"],
})
req = Request(prompt="resuma o repo como JSON", output_schema=schema, output_last_message_path="out.json")
result = runner.run(req) # sync
print(json.loads(result.final_message))
result = await runner.run(req) # asyncreq = Request(prompt="...", dir="/work/project", add_dirs=["/work/shared", "/work/vendor"])
runner.run(req)
await runner.run(req)req = Request(prompt="...", model="o3", profile="work")
runner.run(req)
await runner.run(req)The runner also wraps Claude Code's non-interactive mode
(claude -p --output-format stream-json). Select it with the agent keyword;
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.
import codexcw
runner = codexcw.Runner(agent=codexcw.AGENT_CLAUDE)
result = runner.run(
codexcw.Request(
prompt="crie um arquivo TODO.md",
model=codexcw.CLAUDE_MODEL_HAIKU, # "haiku", "sonnet", or "opus"
permission_mode=codexcw.PERMISSION_ACCEPT_EDITS,
)
)
print("tokens:", result.usage.total_tokens)
print("cache writes:", result.usage.cache_creation_input_tokens)
print("cost (USD):", result.usage.total_cost_usd)
print("per-model usage:", result.usage.model_usage)# Tool filters and resume work per request:
runner.run(
codexcw.Request(
prompt="rode os testes",
model=codexcw.CLAUDE_MODEL_SONNET,
allowed_tools=["Bash(pytest *)", "Read"],
disallowed_tools=["WebSearch"],
)
)
first = runner.run(codexcw.Request(prompt="lembre disto", persistent=True))
runner.run(
codexcw.Request(
prompt="continue",
resume_id=first.thread_id, # or resume_last=True
persistent=True,
)
)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) raise a typed
invalidRequest error on a claude runner.
The exported permission constants cover acceptEdits, auto,
bypassPermissions, manual, dontAsk, and plan.
# Prompt via stdin only:
runner.run(Request(stdin="diga oi"))
await runner.run(Request(stdin="diga oi"))
# Prompt plus extra stdin context (wrapped in <stdin> markers):
runner.run(Request(prompt="resuma o diff abaixo", stdin=large_diff))runner = Runner(executable="/opt/codex/bin/codex", env={"CODEX_HOME": "/tmp/codex-home"})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, in seconds; None uses the 10-second default.
# Sync
from codexcw import AccountUsageRequest, get_account_usage
usage = get_account_usage(AccountUsageRequest(env={"CODEX_HOME": "/tmp/codex-home"}, timeout=5.0))
if usage.account is not None:
print("account:", usage.account.email)
if usage.rate_limits.primary is not None:
print("primary used:", usage.rate_limits.primary.used_percent)
if usage.token_usage is not None:
print("lifetime tokens:", usage.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 is available through the Claude Code /usage command:
from codexcw import ClaudeAccountUsageRequest, get_claude_account_usage
usage = get_claude_account_usage(ClaudeAccountUsageRequest(timeout=5.0))
print(usage.report)
for window in usage.windows:
print(window.label, window.used_percent, window.resets_at)raw keeps the original Claude JSON output, while windows contains the
percentage-based limits parsed from the human-readable report.
# Async
from codexcw import AccountUsageRequest
from codexcw.aio import get_account_usage
usage = await get_account_usage(AccountUsageRequest(env={"CODEX_HOME": "/tmp/codex-home"}))
from codexcw import ClaudeAccountUsageRequest
from codexcw.aio import get_claude_account_usage
claude_usage = await get_claude_account_usage(ClaudeAccountUsageRequest(timeout=5.0))Failures raise a typed CodexcwError whose kind discriminates the cause.
from codexcw import CodexcwError
try:
result = runner.run(Request(prompt="...")) # or: await runner.run(...)
print(result.final_message)
except CodexcwError as err:
if err.kind == "exit":
print(f"agent exited {err.code}: {err.stderr}")
elif err.kind == "codex":
print("codex reported:", err)
elif err.kind == "claude":
print("claude reported:", err)
elif err.kind == "decode":
print(f"bad JSONL on line {err.line}")
elif err.kind == "promptRequired":
print("prompt or stdin is required")
else:
print(err.kind, err)# Sync
session = runner.start(Request(prompt="..."))
session.cancel() # stops the child process
session.wait()
# Async
session = await runner.start(Request(prompt="..."))
session.cancel()
await session.wait()See the README for the cross-language overview and AGENTS.md for the project guide.