openclaw-claude-code supports multiple coding CLI engines behind a unified ISession interface. Each engine manages its own subprocess, event stream, and cost tracking independently.
SessionManager
├── engine: 'claude' → PersistentClaudeSession
│ └── Wraps: claude CLI (stream-json protocol, persistent subprocess)
├── engine: 'codex' → PersistentCodexSession
│ └── Wraps: codex exec --full-auto (per-message spawning)
├── engine: 'gemini' → PersistentGeminiSession
│ └── Wraps: gemini -p --output-format stream-json (per-message spawning)
├── engine: 'cursor' → PersistentCursorSession
│ └── Wraps: agent -p --force --output-format stream-json (per-message spawning)
└── engine: 'custom' → PersistentCustomSession
└── Wraps: any CLI via user-provided CustomEngineConfig
Default engine. Long-running subprocess with streaming JSON I/O.
- Persistent multi-turn conversations
- Real-time streaming (text, tool_use, tool_result events)
- Session resume via
--resume - Full cost tracking from API usage data
await manager.startSession({
name: 'claude-task',
engine: 'claude', // default, can omit
model: 'opus',
cwd: '/project',
});Wraps the codex exec subcommand in full-auto mode. Each send() spawns a new process.
- Non-interactive execution via
codex exec --full-auto - Working directory passed via
-Cflag - One-shot execution per message (no persistent subprocess)
- Working directory carries accumulated changes across sends
- Token estimation from response length (~4 chars/token)
- Requires
codexCLI >= 0.112:npm install -g @openai/codex
await manager.startSession({
name: 'codex-task',
engine: 'codex',
model: 'gpt-5.4',
cwd: '/project',
});Wraps the gemini CLI with --output-format stream-json. Each send() spawns a new process.
- One-shot execution per message (no persistent subprocess)
- Working directory carries accumulated changes across sends
- Real token counts from stream-json
resultevents (not estimated) - Permission modes:
bypassPermissions→--yolo,default→--sandbox - Requires
geminiCLI installed:npm install -g @google/gemini-cli
await manager.startSession({
name: 'gemini-task',
engine: 'gemini',
model: 'gemini-3.1-pro-preview',
cwd: '/project',
});Wraps the Cursor Agent CLI (agent) with --print --force --output-format stream-json. Each send() spawns a new process.
- One-shot execution per message (no persistent subprocess)
- Working directory via
--workspaceflag - Real token counts from stream-json
resultevents (camelCase:inputTokens,outputTokens,cacheReadTokens) --forceenables auto-approval of all file changes--trustauto-trusts the workspace without prompting- Cursor uses its own model routing (e.g.,
sonnet-4,gpt-5,auto) - Requires Cursor Agent CLI:
curl https://cursor.com/install -fsSL | bash - Binary:
agent(setCURSOR_BINenv var to override)
await manager.startSession({
name: 'cursor-task',
engine: 'cursor',
model: 'sonnet-4',
cwd: '/project',
});All engines implement ISession, making them interchangeable at the SessionManager level:
interface ISession {
// State
sessionId?: string;
readonly isReady: boolean;
readonly isPaused: boolean;
readonly isBusy: boolean;
// Lifecycle
start(): Promise<this>;
stop(): void;
pause(): void;
resume(): void;
// Communication
send(message, options?): Promise<TurnResult | { requestId; sent }>;
// Observability
getStats(): SessionStats & { sessionId?; uptime };
getHistory(limit?): Array<{ time; type; event }>;
getCost(): CostBreakdown;
// Context
compact(summary?): Promise<TurnResult | { requestId; sent }>;
getEffort(): EffortLevel;
setEffort(level): void;
// Model
resolveModel(alias): string;
// Events (EventEmitter)
on(event, listener): this;
emit(event, ...args): boolean;
}Team tools (team_list, team_send) work on all engines with engine-appropriate implementations:
| Engine | team_list |
team_send |
|---|---|---|
| Claude | Native /team command |
Native @teammate command |
| Codex | Lists other active SessionManager sessions | Routes via cross-session inbox |
| Gemini | Lists other active SessionManager sessions | Routes via cross-session inbox |
| Cursor | Lists other active SessionManager sessions | Routes via cross-session inbox |
For Codex, Gemini, and Cursor, the "team" is the set of all active sessions managed by SessionManager. Messages are delivered via the inbox system — idle sessions receive immediately, busy sessions queue for later delivery.
Claude Code CLI only speaks Anthropic protocol. The built-in proxy translates Anthropic ↔ OpenAI format, letting you drive Claude Code with any model routed through the OpenClaw gateway.
If OpenClaw gateway is running, everything is automatic:
// No baseUrl, no env vars, no extra config
await manager.startSession({
name: 'task',
engine: 'claude',
model: 'openclaw', // gateway routes to your configured model
cwd: '/project',
});What happens behind the scenes:
- Plugin reads
~/.openclaw/openclaw.jsonfor gateway port + auth - Starts a local proxy server (random port, auto-managed)
- Claude Code CLI sends Anthropic-format requests → proxy converts to OpenAI → gateway → any model
Override with environment variables if needed:
| Variable | Default | Description |
|---|---|---|
GATEWAY_URL |
Auto-detected from openclaw.json | Gateway endpoint (e.g. http://127.0.0.1:18789/v1) |
GATEWAY_KEY |
Auto-detected from openclaw.json | Gateway auth password/token |
GEMINI_API_KEY |
- | Direct Gemini API access (bypasses gateway) |
OPENAI_API_KEY |
- | Direct OpenAI API access (bypasses gateway) |
Claude Code CLI (Anthropic format)
→ Auto-proxy (Anthropic → OpenAI conversion)
→ OpenClaw Gateway (/v1/chat/completions, model="openclaw")
→ Any model (Gemini, GPT, local, etc.)
Integrate any coding agent CLI without writing engine-specific code. You provide a CustomEngineConfig that maps your CLI's flags to OpenClaw session concepts.
Two protocol modes:
- Persistent (
persistent: true) — long-running subprocess with stream-json I/O over stdin/stdout (like Claude Code) - One-shot (
persistent: false, default) — new process spawned persend()(like Gemini/Codex)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Display name (used in logs, session IDs) |
bin |
string | yes | Binary path or command name |
binEnv |
string | Env var name that overrides bin at runtime |
|
persistent |
boolean | true = persistent subprocess, false = one-shot (default) |
|
args |
object | yes | CLI flag mappings (see below) |
permissionModes |
object | Maps OpenClaw mode names to CLI-specific values | |
pricing |
object | { input, output, cached? } per 1M tokens |
|
contextWindow |
number | Context window size (default: 200,000) | |
env |
object | Extra environment variables for the CLI process | |
sanitizePatterns |
string[] | Regex patterns to redact from stderr |
| Key | Example | Description |
|---|---|---|
print |
"-p" |
Non-interactive/print mode flag |
outputFormat |
"--output-format" |
Output format flag |
outputFormatValue |
"stream-json" |
Value for stream-json output |
inputFormat |
"--input-format" |
Input format flag (persistent only) |
inputFormatValue |
"stream-json" |
Value for stream-json input |
skipPermissions |
"-y" |
Skip all permissions flag |
permissionMode |
"--permission-mode" |
Permission mode flag |
model |
"--model" |
Model selection flag |
systemPrompt |
"--system-prompt" |
System prompt override flag |
appendSystemPrompt |
"--append-system-prompt" |
Append system prompt flag |
maxTurns |
"--max-turns" |
Max agent turns flag |
resume |
"--resume" |
Session resume flag (persistent only) |
verbose |
"--verbose" |
Verbose output flag |
replayUserMessages |
"--replay-user-messages" |
Replay user messages (persistent only) |
includePartialMessages |
"--include-partial-messages" |
Include partial messages (persistent only) |
effort |
"--effort" |
Effort level flag |
workspace |
"--workspace" |
Workspace/cwd flag (one-shot only) |
extra |
["--trust"] |
Additional static arguments |
await manager.startSession({
name: 'my-agent-task',
engine: 'custom',
cwd: '/project',
customEngine: {
name: 'my-agent',
bin: 'my-agent',
binEnv: 'MY_AGENT_BIN',
persistent: true,
args: {
print: '-p',
outputFormat: '--output-format',
outputFormatValue: 'stream-json',
inputFormat: '--input-format',
inputFormatValue: 'stream-json',
skipPermissions: '-y',
permissionMode: '--permission-mode',
model: '--model',
systemPrompt: '--system-prompt',
appendSystemPrompt: '--append-system-prompt',
maxTurns: '--max-turns',
resume: '--resume',
verbose: '--verbose',
replayUserMessages: '--replay-user-messages',
includePartialMessages: '--include-partial-messages',
},
pricing: { input: 3, output: 15, cached: 0.3 },
contextWindow: 200_000,
sanitizePatterns: ['MY_API_KEY=[^\\s]+'],
},
});await manager.startSession({
name: 'simple-agent-task',
engine: 'custom',
cwd: '/project',
customEngine: {
name: 'simple-agent',
bin: '/usr/local/bin/simple-agent',
persistent: false, // default
args: {
print: '-p',
outputFormat: '--output-format',
outputFormatValue: 'stream-json',
skipPermissions: '--yolo',
model: '--model',
workspace: '--workspace',
extra: ['--no-color'],
},
permissionModes: {
bypassPermissions: 'yolo',
default: 'sandbox',
},
pricing: { input: 1, output: 5 },
},
});Custom engines work in council by setting engine: 'custom' and customEngine on the agent persona:
manager.councilStart('Build feature X', {
agents: [
{
name: 'Planner',
emoji: '🟠',
persona: 'Architecture expert',
engine: 'custom',
customEngine: { name: 'my-agent', bin: 'my-agent', persistent: true, args: { ... } },
},
{ name: 'Reviewer', emoji: '🔵', persona: 'Code reviewer', engine: 'claude', model: 'opus' },
],
maxRounds: 10,
projectDir: '/project',
});To add a built-in engine (for CLIs that need custom protocol handling beyond what CustomEngineConfig supports):
- Create
src/persistent-<engine>-session.tsimplementingISession - Add the engine name to
EngineTypeinsrc/types.ts - Add a case to
SessionManager._createSession() - Add model pricing to
MODELS[]insrc/models.ts
The ISession interface is deliberately minimal — each engine handles its own subprocess bootstrapping, I/O protocol, and cleanup internally.
For most third-party CLIs, the custom engine with CustomEngineConfig is sufficient and requires zero code changes.