Coding Agent Wrapper — a Python library and CLI for orchestrating coding agents (Claude Code, Codex, opencode) with a unified interface, MCP tool servers, and credential management for Docker containers.
pip install coding-agent-wrapperImport caw:
import cawFor local development:
pip install -e .Requires Python 3.10+.
caw wraps multiple coding agent CLIs behind a single Agent / Session API. Swap providers without changing your code.
from caw import Agent
agent = Agent() # defaults to claude_code
traj = agent.completion("Explain what this repository does")
print(traj.result)
print(f"{traj.usage.total_tokens} tokens, ${traj.usage.cost_usd:.4f}")from caw import Agent
agent = Agent(provider="claude_code", model="opus", reasoning="high")
agent.set_system_prompt("You are a security reviewer.")
with agent.start_session() as session:
turn1 = session.send("Review src/auth.py for vulnerabilities")
print(turn1.result)
turn2 = session.send("Now check src/api.py")
print(turn2.result)
# session.end() called automatically, returns full TrajectoryGrab a resume_handle (a string) and store it anywhere — a database, a file, a
queue. Later, in a different process, resume the conversation:
# Process 1: start, communicate, persist the handle.
agent = Agent(provider="claude_code")
session = agent.start_session()
session.send("My deploy target is staging-eu. Remember that.")
handle = session.resume_handle # store this string
session.end()
# Process 2 (later, after a restart): resume by handle.
agent = Agent(provider="claude_code")
session = agent.resume_session(handle)
print(session.send("Where am I deploying?").result) # -> "staging-eu"
session.end()The handle is a self-contained JSON string carrying the backend's own resume
key, so resuming works even with no data_dir — the underlying CLI still has the
conversation:
{"version": 1, "provider": "claude_code", "session_id": "bd260210-…", "resume_key": "bd260210-…"}(resume_key is claude's session id, Codex's thread_id, or opencode's session
id — for codex/opencode it differs from session_id.) Send at least one message
before reading resume_handle; the backend assigns its key on the first
exchange. Works across all three providers.
The handle grants resume access to the conversation — treat it like a secret, not an opaque random id.
data_dir is optional and additive:
without data_dir |
with the original data_dir |
|
|---|---|---|
| backend conversation | resumed | resumed |
| caw trajectory | starts empty | full history restored |
| new turns | not persisted | appended to the original session dir |
| Provider | CLI | Provider name |
|---|---|---|
| Claude Code | claude |
claude_code |
| Codex | codex |
codex |
| opencode | opencode |
opencode |
Set via constructor, environment variable, or at runtime:
agent = Agent(provider="codex")
# or
os.environ["CAW_PROVIDER"] = "codex"
# or
agent.set_provider("codex")Attach MCP servers so the agent can call external tools:
from caw import Agent, MCPServer
agent = Agent()
agent.add_mcp_server(MCPServer(
name="my_db",
command="python",
args=["-m", "my_mcp_server"],
))Define tools as Python classes. caw spins up an HTTP MCP server automatically:
from caw import Agent, ToolKit, tool
class UserDB(ToolKit, server_name="user_db"):
def __init__(self):
self.users = ["Alice", "Bob"]
@tool(description="List all users")
async def list_users(self) -> str:
return ", ".join(self.users)
@tool(description="Add a user")
async def add_user(self, name: str) -> str:
self.users.append(name)
return f"Added {name}"
db = UserDB()
agent = Agent(system_prompt="You have access to a user database.")
agent.add_tool_server(db.as_server())
traj = agent.completion("Add Eve to the user database, then list all users")Register child agents that the parent can invoke as tools:
from caw import Agent, AgentSpec
reviewer = AgentSpec(
name="security_reviewer",
description="Reviews code for security issues",
system_prompt="You are a security expert. Review the given code.",
)
agent = Agent()
agent.add_subagent(reviewer)
traj = agent.completion("Review the auth module for vulnerabilities")
# Subagent trajectories are captured:
for sub in traj.subagent_trajectories:
print(f" subagent: {sub.agent}, {sub.num_turns} turns")Every interaction produces a Trajectory with structured data:
Trajectory
├── agent, model, session_id, created_at
├── turns: list[Turn]
│ ├── input: str
│ ├── output: list[TextBlock | ThinkingBlock | ToolUse]
│ │ └── ToolUse.subagent_trajectory: Trajectory | None
│ ├── usage: UsageStats
│ └── duration_ms: int
├── usage: UsageStats (own)
└── total_usage: UsageStats (own + all nested subagents)
Sessions are persisted to JSONL in caw_data/ by default.
| Variable | Purpose |
|---|---|
CAW_PROVIDER |
Default provider (claude_code, codex) |
CAW_MODEL |
Default model name |
CAW_EFFORT |
Default reasoning effort (high, medium, low) |
Manages coding agent OAuth credentials so they stay in sync between your host and Docker containers. Supports Claude Code, Codex, and opencode. Host credential files are never modified — they are bind-mounted into the container at run time.
caw auth setup # snapshot configs, write mount manifest
caw auth status # token expiry, last modified, mount flags
docker run $(caw auth docker-flags) -v ./project:/work my-image
caw auth teardown # rm -rf ~/.caw/auth/ (host files untouched)See caw/auth/README.md for details on how it works, container setup, and supported agents.