Client-Server Architecture
Feature Definition
Section titled “Feature Definition”AI coding agents need to mediate between user interfaces (terminal, web, IDE extension) and backend logic (LLM calls, file operations, session state). The fundamental question is whether this mediation happens inside a single process or across a network boundary.
A single-process monolith is simpler to deploy and debug, but it couples the rendering loop to the agent loop. A client-server split lets multiple frontends attach to one agent backend, enables remote access, and makes the TUI replaceable without touching the core. The trade-off is complexity: HTTP routing, serialization, event streaming, authentication, and process lifecycle management.
Of the three reference implementations, only OpenCode chose the client-server path. Aider and Codex are single-process architectures where the TUI (or CLI) runs in the same process as the agent. This page traces all three designs in depth.
For the dedicated SKILL.md subsystem that sits on top of these architectures (including OpenCode’s /skill routes), see Skills.
For MCP lifecycle management routes and status events in the server layer, see MCP Integration. For Codex’s stdio MCP server mode, see MCP Server.
Aider Implementation
Section titled “Aider Implementation”Pinned at: b9050e1d (aider)
Aider is a single-process Python application. There is no server, no HTTP, and no separation between the UI and agent logic.
Process Model
Section titled “Process Model”The entry point is aider/main.py, which constructs a Coder instance and calls coder.run(). The run() method is a synchronous loop that reads input from prompt_toolkit, sends it to the LLM, processes the response, applies edits, and prints output — all in the same thread. The only concurrency is a background thread for chat history summarization (ChatSummary).
There is no mechanism for a second process to connect to a running Aider session. The --watch-files mode uses watchfiles to detect changes but still operates within the same process.
Why No Server?
Section titled “Why No Server?”Aider’s design assumes one human, one terminal, one session. The prompt_toolkit input loop is inherently tied to a single terminal. There is no IDE extension, no web UI, and no remote access. The simplicity is intentional — Aider’s value proposition is “run it in your terminal and it edits your code.”
Implications for OpenOxide
Section titled “Implications for OpenOxide”Aider demonstrates that a single-process model is viable for a terminal-only tool. The cost is that you cannot build a web UI, IDE extension, or remote access without rearchitecting the core.
Codex Implementation
Section titled “Codex Implementation”Pinned at: 4ab44e2c (codex)
Codex is a single-process Rust application. The TUI and agent loop coexist in one tokio runtime within the same binary.
Process Model
Section titled “Process Model”The entry point is codex-rs/tui/src/lib.rs:129 (run_main()). This function:
- Parses CLI flags and loads config via
ConfigBuilder - Constructs a
codex_core::Configwith sandbox mode, approval policy, and MCP settings - Creates the
Appstruct (the ratatui TUI application) - Enters the ratatui event loop
The App struct owns both the rendering state and a handle to codex_core::Codex, the agent engine. The agent runs as a set of tokio tasks inside the same runtime. Communication between the TUI and agent is via async channels (async_channel::Sender<Event>), not HTTP.
┌─────────────────────────────────────────────┐│ Single Process (tokio) ││ ││ ┌──────────┐ async_channel ┌────────┐ ││ │ TUI │ ◄────────────────► │ Agent │ ││ │ (ratatui)│ │ Loop │ ││ └──────────┘ └────────┘ ││ │ │ ││ crossterm OpenAI API ││ terminal I/O + tool calls │└─────────────────────────────────────────────┘Key Architecture Points
Section titled “Key Architecture Points”Channel-based communication. The TUI subscribes to Event messages from the agent via async_channel. Events include ResponseItem (LLM output), TaskStarted, TaskComplete, McpStartupUpdate, and ApprovalRequest. The TUI does not poll; it receives events as they arrive.
No HTTP surface in normal mode. When you run codex interactively, there is no HTTP server. The binary is self-contained.
The MCP server exception. Codex can run as an MCP server via codex mcp-server, which is a separate binary entry point (codex-rs/mcp-server/src/lib.rs:51). In this mode, Codex reads JSON-RPC from stdin and writes to stdout — it becomes a stdio transport MCP server. This is NOT an HTTP server; it is designed to be spawned as a child process by an MCP client (like Claude Code or another agent). The architecture is covered in detail on the MCP Server page.
Headless mode. The codex exec subcommand runs the agent without a TUI, printing output to stdout. It reuses the same codex_core::Codex engine but skips the ratatui rendering. This is still a single process.
The codex-cli Node Wrapper
Section titled “The codex-cli Node Wrapper”The codex-cli/ directory contains a Node.js wrapper that downloads the platform-appropriate Rust binary and spawns it as a child process. This is purely for npm distribution convenience — the Node process does not participate in the agent logic. It calls child_process.spawn() with the Rust binary and forwards stdin/stdout.
Why No Server?
Section titled “Why No Server?”Codex was built by OpenAI for their own use case: a terminal tool that interacts with the Responses API. The single-process model keeps latency low (no serialization overhead), simplifies deployment (one binary), and avoids the complexity of HTTP routing and authentication. The codex mcp-server mode provides programmatic access without building a full HTTP API.
OpenCode Implementation
Section titled “OpenCode Implementation”Pinned at: 7ed44997 (opencode)
OpenCode is the only reference implementation with a true client-server architecture. The server is a Hono HTTP application running on Bun, and the TUI is a separate process that connects via HTTP and SSE.
Process Model
Section titled “Process Model”┌──────────────────────┐ ┌──────────────────────────────┐│ TUI Client │ HTTP │ Server ││ (OpenTUI + Solid.js)│ ◄─────► │ (Hono + Bun on :4096) ││ │ SSE │ ││ Uses SDK generated │ ◄────── │ Bus → SSE event stream ││ from OpenAPI spec │ │ ││ │ WS │ PTY sessions via WebSocket ││ │ ◄─────► │ │└──────────────────────┘ └──────────────────────────────┘ │ LLM APIs, MCP servers, file I/O, git, LSPTwo separate processes:
-
Server process — launched via
opencode serve(packages/opencode/src/cli/cmd/serve.ts). Runs the Hono HTTP app on port 4096 (with fallback to OS-assigned port). Manages all business logic: sessions, LLM calls, tool execution, MCP connections, LSP clients, file operations, git. -
TUI client process — launched via
opencode(the default command) oropencode attach <url>. Connects to the server via HTTP. Renders using OpenTUI (Zig rendering engine + Solid.js component model). The TUI does not run any agent logic — it is purely a frontend.
Server Setup
Section titled “Server Setup”The server is defined in packages/opencode/src/server/server.ts. The App is a Hono instance with a layered middleware stack:
Middleware chain (applied in order, server.ts:62-212):
-
Error handler (
server.ts:62-79) — catches all exceptions, convertsNamedErrorsubclasses to appropriate HTTP status codes (404 forNotFoundError, 400 for model/worktree errors, 500 for unknown). Stack traces included in 500 responses. -
Basic auth (
server.ts:80-88) — optional, activated byOPENCODE_SERVER_PASSWORDenv var. Username defaults toopencode(configurable viaOPENCODE_SERVER_USERNAME). Useshono/basic-auth. CORS preflight (OPTIONS) bypasses auth. -
Request logging (
server.ts:89-105) — logs method and path, measures request duration. Skips logging forPOST /log(client-side log forwarding). -
CORS (
server.ts:106-131) — whitelist approach:http://localhost:*andhttp://127.0.0.1:*(any port)tauri://localhost,http://tauri.localhost,https://tauri.localhost*.opencode.ai(HTTPS only, regex validated)- Custom origins via
opts.corsparameter
-
Directory resolver (
server.ts:195-212) — extracts working directory from query parameter (?directory=...), HTTP header (x-opencode-directory), or falls back toprocess.cwd(). Creates a per-directoryInstancecontext viaInstance.provide(). This is how one server serves multiple project directories simultaneously.
Route mounting (server.ts:227-237):
.route("/global", GlobalRoutes()).route("/project", ProjectRoutes()).route("/pty", PtyRoutes()).route("/config", ConfigRoutes()).route("/experimental", ExperimentalRoutes()).route("/session", SessionRoutes()).route("/permission", PermissionRoutes()).route("/question", QuestionRoutes()).route("/provider", ProviderRoutes()).route("/", FileRoutes()).route("/mcp", McpRoutes()).route("/tui", TuiRoutes())All route modules use lazy() for deferred instantiation — routes are only built when first accessed.
Catch-all proxy (server.ts:543-558) — any path not matched by the API routes is proxied to https://app.opencode.ai, which serves the web UI. This means the server doubles as a web frontend host, with a Content-Security-Policy header injected.
Server listen (server.ts:576-622):
export function listen(opts: { port, hostname, mdns?, mdnsDomain?, cors? }) { const tryServe = (port) => Bun.serve({ hostname, idleTimeout: 0, fetch: App().fetch, websocket }) const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) // Try 4096 first, then OS-assigned : tryServe(opts.port)}Port negotiation: if port 0 is requested (automatic), it tries 4096 first for predictability, then falls back to any available port. idleTimeout: 0 disables connection idle timeouts (important for SSE streams). Optional mDNS publishing for LAN discovery is supported but only when hostname is not loopback.
Route Inventory
Section titled “Route Inventory”OpenCode exposes 100+ HTTP endpoints across 12 route modules plus server-level routes. The full inventory:
Session Routes (routes/session.ts, ~900 lines, 19+ endpoints)
Section titled “Session Routes (routes/session.ts, ~900 lines, 19+ endpoints)”The largest route module. Manages the full session lifecycle:
| Method | Path | Purpose |
|---|---|---|
| GET | /session/ | List sessions (filters: directory, roots, start, search, limit) |
| POST | /session/ | Create new session |
| GET | /session/:sessionID | Get session details |
| DELETE | /session/:sessionID | Delete session |
| PATCH | /session/:sessionID | Update session (title, archive time) |
| GET | /session/:sessionID/children | List forked child sessions |
| GET | /session/:sessionID/message | List messages (with limit) |
| POST | /session/:sessionID/message | Send message to AI (streaming response) |
| POST | /session/:sessionID/prompt_async | Send message asynchronously (returns 204) |
| POST | /session/:sessionID/abort | Cancel ongoing AI processing |
| POST | /session/:sessionID/fork | Fork session at a message |
| POST | /session/:sessionID/share | Create shareable link |
| DELETE | /session/:sessionID/share | Revoke share |
| POST | /session/:sessionID/summarize | Trigger LLM compaction |
| GET | /session/:sessionID/diff | Get file diffs from a message |
| GET | /session/status | All active/idle/completed states |
The critical endpoint is POST /session/:sessionID/message. It uses Hono’s stream() to hold the connection open while the LLM processes, then writes the complete message JSON at the end. This is NOT server-sent events — it is a single streamed JSON response. Real-time deltas arrive separately via the SSE event stream.
Permission Routes (routes/permission.ts, 69 lines)
Section titled “Permission Routes (routes/permission.ts, 69 lines)”| Method | Path | Purpose |
|---|---|---|
| GET | /permission/ | List pending permission requests |
| POST | /permission/:requestID/reply | Approve or deny (body: {reply, message?}) |
The permission flow is event-driven: the agent publishes a permission.requested bus event, the TUI receives it via SSE, displays the prompt, and posts the reply back via HTTP. The request ID is consumed on reply.
TUI Control Routes (routes/tui.ts, ~380 lines)
Section titled “TUI Control Routes (routes/tui.ts, ~380 lines)”A bidirectional control channel for TUI-specific operations:
| Method | Path | Purpose |
|---|---|---|
| POST | /tui/append-prompt | Inject text into the TUI prompt |
| POST | /tui/submit-prompt | Submit current prompt |
| POST | /tui/clear-prompt | Clear prompt input |
| POST | /tui/execute-command | Execute TUI command (agent_cycle, session_new, etc.) |
| POST | /tui/show-toast | Display toast notification |
| POST | /tui/open-help | Open help dialog |
| POST | /tui/open-sessions | Open session picker |
| POST | /tui/open-themes | Open theme picker |
| POST | /tui/open-models | Open model picker |
| POST | /tui/select-session | Navigate to a session |
| GET | /tui/control/next | Blocking long-poll — get next TUI request |
| POST | /tui/control/response | Submit response to a TUI request |
| POST | /tui/publish | Publish arbitrary event |
The control/next + control/response pair implements a request-response pattern over REST using an AsyncQueue. The server pushes a request to the queue, then GET /tui/control/next blocks until it arrives. The client processes it and posts the response to /tui/control/response.
PTY Routes (routes/pty.ts, 196 lines)
Section titled “PTY Routes (routes/pty.ts, 196 lines)”| Method | Path | Purpose |
|---|---|---|
| GET | /pty/ | List PTY sessions |
| POST | /pty/ | Create PTY session |
| GET | /pty/:ptyID | Get PTY details |
| PUT | /pty/:ptyID | Update PTY |
| DELETE | /pty/:ptyID | Remove PTY |
| GET (WS) | /pty/:ptyID/connect | WebSocket — real-time terminal I/O |
The WebSocket connection at /pty/:ptyID/connect supports a cursor query parameter for replaying terminal history from a checkpoint. This is the only WebSocket endpoint in the entire server.
Other Route Modules
Section titled “Other Route Modules”- Global (
routes/global.ts) —GET /global/health(health check with version),GET /global/event(cross-instance SSE stream) - File (
routes/file.ts) —GET /find(ripgrep search),GET /find/file(file search),GET /file(directory listing),GET /file/content(read file),GET /file/status(git status) - Config (
routes/config.ts) —GET /config/(full config),PATCH /config/(update config),GET /config/providers(provider list) - Project (
routes/project.ts) —GET /project/(list projects),GET /project/current,PATCH /project/:projectID - Provider (
routes/provider.ts) —GET /provider/(list providers),GET /provider/auth,POST /provider/:providerID/oauth/authorize,POST /provider/:providerID/oauth/callback - MCP (
routes/mcp.ts) — covered in detail on the MCP Integration page - Question (
routes/question.ts) —GET /question/(list pending),POST /question/:requestID/reply,POST /question/:requestID/reject - Experimental (
routes/experimental.ts) —GET /experimental/tool/ids,GET /experimental/tool(with schemas),POST /experimental/worktree
Server-Level Endpoints (in server.ts itself)
Section titled “Server-Level Endpoints (in server.ts itself)”| Method | Path | Purpose |
|---|---|---|
| PUT | /auth/:providerID | Set auth credentials |
| DELETE | /auth/:providerID | Remove auth credentials |
| GET | /doc | OpenAPI specification (auto-generated) |
| GET | /command | List available commands |
| GET | /agent | List available agents |
| GET | /skill | List available skills |
| GET | /lsp | LSP server status |
| GET | /formatter | Formatter status |
| GET | /event | SSE event stream (instance-scoped) |
| GET | /path | Get paths (home, state, config, worktree, directory) |
| GET | /vcs | VCS info (git branch) |
| POST | /log | Client log forwarding |
| POST | /instance/dispose | Dispose instance and cleanup |
Event System
Section titled “Event System”OpenCode’s real-time communication uses a two-tier event bus:
Instance-scoped Bus (bus/index.ts) — per-directory event pub/sub. The Bus.publish() function dispatches to local subscribers and also forwards to GlobalBus. Subscribers can listen to specific event types or use Bus.subscribeAll() for a wildcard subscription. The bus is tied to Instance.state(), so each directory gets its own set of subscriptions.
GlobalBus (bus/global.ts) — cross-instance event aggregation. Events from all directories flow here. Used by GET /global/event for clients that need to watch multiple projects.
SSE stream (server.ts:486-541):
.get("/event", async (c) => { c.header("X-Accel-Buffering", "no") c.header("X-Content-Type-Options", "nosniff") return streamSSE(c, async (stream) => { stream.writeSSE({ data: JSON.stringify({ type: "server.connected", properties: {} }) }) const unsub = Bus.subscribeAll(async (event) => { await stream.writeSSE({ data: JSON.stringify(event) }) if (event.type === Bus.InstanceDisposed.type) stream.close() }) const heartbeat = setInterval(() => { stream.writeSSE({ data: JSON.stringify({ type: "server.heartbeat", properties: {} }) }) }, 10_000) await new Promise<void>((resolve) => { stream.onAbort(() => { clearInterval(heartbeat); unsub(); resolve() }) }) })})Key details: the stream sends server.connected immediately on connection, heartbeats every 10 seconds (to prevent proxy/CDN timeouts), and auto-closes when the instance is disposed. X-Accel-Buffering: no disables Nginx buffering.
Client SDK
Section titled “Client SDK”OpenCode generates a TypeScript SDK from its OpenAPI specification (packages/sdk/). The TUI client uses this SDK for all server communication:
const sdk = createOpencodeClient({ baseUrl: props.url, // e.g., "http://localhost:4096" signal: abort.signal, directory: props.directory, fetch: props.fetch, // Custom fetch with auth headers headers: props.headers,})The SDK provides typed methods for every endpoint (sdk.session.get(), sdk.session.prompt(), sdk.event.subscribe(), etc.).
TUI Attachment
Section titled “TUI Attachment”The opencode attach <url> command connects the TUI to an existing server:
await tui({ url: args.url, args: { continue: args.continue, sessionID: args.session, fork: args.fork }, directory, headers: { Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` },})The TUI is purely a rendering layer — all state lives on the server. Multiple TUI clients can attach to the same server (there is no exclusive lock), though behavior with concurrent users on the same session is undefined.
Instance-Per-Directory Isolation
Section titled “Instance-Per-Directory Isolation”The directory resolver middleware (server.ts:195-212) creates a scoped Instance context for each request based on the directory parameter. This means:
- Each project directory has its own sessions, MCP connections, LSP servers, and bus subscriptions
- One server process can serve multiple projects simultaneously
- No cross-directory state leakage (enforced by
Instance.provide()) - No user/tenant isolation — directory is the only boundary
Claude Code Implementation
Section titled “Claude Code Implementation”Claude Code is closed-source, so architecture is inferred from public documentation.
Process Model
Section titled “Process Model”Claude Code uses a host-process model rather than a client-server split. The CLI binary (claude) is the canonical runtime. All other surfaces — VS Code extension, JetBrains plugin, Desktop app — wrap the CLI as a child process and communicate via its stdin/stdout or integration protocol.
┌───────────────────────────────────────────────────────────────────┐│ User Surface ││ ││ ┌───────────┐ ┌───────────────┐ ┌────────────┐ ┌──────────┐ ││ │ Terminal │ │ VS Code Ext │ │ JetBrains │ │ Desktop │ ││ │ (claude) │ │ (hosts CLI) │ │ (hosts CLI)│ │ (Code │ ││ │ │ │ │ │ │ │ tab) │ ││ └─────┬─────┘ └──────┬────────┘ └─────┬──────┘ └────┬─────┘ ││ │ │ │ │ ││ └───────────────┼─────────────────┼───────────────┘ ││ ▼ ││ ┌──────────────────┐ ││ │ Claude Code CLI │ ││ │ (Agent Runtime) │ ││ └────────┬─────────┘ ││ │ ││ Anthropic API ││ + local tool calls │└───────────────────────────────────────────────────────────────────┘This is the inverse of OpenCode’s architecture: instead of a server that frontends connect to over HTTP, Claude Code is a process that host applications embed directly.
IDE Integration Protocol
Section titled “IDE Integration Protocol”Both the VS Code extension and JetBrains plugin follow the same pattern:
- Extension/plugin provides GUI chrome (panel, diff viewer, diagnostic forwarding)
- Claude Code CLI runs as a child process underneath
- IDE-specific features (inline diff, selection context, @-mention resolution, diagnostic sharing) are communicated via integration protocol
- Configuration is shared via
~/.claude.jsonand~/.claude/settings.json - The
/idecommand connects an external CLI session to a running IDE instance
The VS Code extension also supports a terminal mode (useTerminal setting) where it falls back to the CLI interface inside a VS Code terminal panel.
Programmatic Interface (Agent SDK)
Section titled “Programmatic Interface (Agent SDK)”The -p (print) flag transforms the CLI into a non-interactive execution engine:
claude -p "Summarize this project" --output-format jsonThree output formats:
text(default): Plain text responsejson: Structured JSON withresult,session_id,structured_output(when--json-schemaused), and usage metadatastream-json: Newline-delimited JSON events for real-time streaming (combine with--verbose --include-partial-messages)
Session continuity works in programmatic mode via --continue (most recent) or --resume <session_id> (specific).
Python and TypeScript SDK packages extend this with native callbacks, structured outputs, and message objects.
Cloud Execution Architecture
Section titled “Cloud Execution Architecture”Claude Code on the web (claude.ai/code) and Desktop remote sessions run on Anthropic-managed infrastructure:
┌──────────────────┐ ┌─────────────────────────────────────┐│ User Surface │ │ Anthropic Cloud ││ │ │ ││ claude.ai/code ──┼──────┼──► ┌──────────────────────────────┐ ││ Desktop (remote)─┼──────┼──► │ Isolated VM per session │ ││ iOS app ─────────┼──────┼──► │ │ ││ & prefix (CLI) ──┼──────┼──► │ ┌──────────┐ ┌──────────┐ │ ││ │ │ │ │ Claude │ │ Git │ │ ││ │ │ │ │ Code CLI │ │ Proxy │ │ ││ │ │ │ └─────┬────┘ └────┬─────┘ │ ││ │ │ │ │ │ │ ││ │ │ │ ┌─────┴────────────┴─────┐ │ ││ │ │ │ │ Security Proxy │ │ ││ │ │ │ │ (domain allowlisting) │ │ ││ │ │ │ └────────────────────────┘ │ ││ │ │ └──────────────────────────────┘ ││ │ └─────────────────────────────────────┘└──────────────────┘Key architectural properties:
- VM isolation: Each session runs in its own VM with a universal dev image (Python, Node, Ruby, Go, Rust, Java, C++, PostgreSQL 16, Redis 7)
- Git proxy: All git operations route through a dedicated proxy. Scoped credentials inside the sandbox; proxy translates to actual GitHub auth tokens. Push restricted to current working branch.
- Security proxy: All HTTP/HTTPS outbound traffic through proxy for abuse prevention and rate limiting. Three access levels: No internet, Limited (domain allowlist), Full.
- SessionStart hooks: Custom dependency installation via hooks (
CLAUDE_CODE_REMOTEenv var to scope remote-only) - Session persistence: Sessions continue even if the user disconnects. Accessible from web, Desktop, iOS, and CLI via
/teleport.
Session Mobility
Section titled “Session Mobility”Claude Code supports cross-surface session movement:
| Transition | Mechanism | What Transfers |
|---|---|---|
| CLI -> Web | & prefix or --remote | New session with current context |
| Web -> CLI | /teleport or --teleport | Branch checkout + conversation history |
| CLI -> Desktop | /desktop | Full session handoff |
| CLI -> IDE | /ide | Connects to running IDE |
| Desktop -> Web | ”Continue in” menu | Pushes branch, generates summary |
| IDE <-> CLI | --resume | Shared conversation history (bidirectional) |
Most transitions create new sessions with varying context transfer. Only IDE <-> CLI shares actual conversation history.
Enterprise Governance and Observability
Section titled “Enterprise Governance and Observability”Claude Code provides a layered governance stack for enterprise deployment:
Server-Managed Settings (Public Beta)
Administrators configure Claude Code centrally via claude.ai Admin Settings web UI. Settings are delivered from Anthropic’s servers at authentication time and cached locally.
- Supports all
settings.jsonkeys plus managed-only keys (disableBypassPermissionsMode) - Settings precedence: server-managed > endpoint-managed (MDM) > user/project
- Startup + hourly polling; cached settings persist through network failures
- Security dialogs for shell commands, custom env vars, hook configs (user must approve or Claude Code exits)
- Non-interactive mode (
-p) skips security dialogs - Not available on Bedrock/Vertex/Foundry/custom
ANTHROPIC_BASE_URL - Audit logging via compliance API
- Owner/Primary Owner roles only
- Limitations: uniform for all org users (no per-group), no MCP server configs via this channel
Two managed settings approaches:
| Approach | Best For | Security Model |
|---|---|---|
| Server-managed | Organizations without MDM | Delivered from Anthropic servers at auth |
| Endpoint-managed | Organizations with MDM | OS-protected file in system directory |
OpenTelemetry Observability
Claude Code exports telemetry via standard OpenTelemetry protocols when CLAUDE_CODE_ENABLE_TELEMETRY=1:
- Exporters: otlp, prometheus, console (metrics); otlp, console (logs/events)
- 8 metrics:
session.count,lines_of_code.count,pull_request.count,commit.count,cost.usage(USD),token.usage,code_edit_tool.decision,active_time.total - 5 events:
user_prompt,tool_result,api_request,api_error,tool_decision - Standard attributes: session.id, organization.id, user.account_uuid, user.email, terminal.type
- Event correlation via
prompt.id(UUID linking all events from one user prompt) - Multi-team segmentation via
OTEL_RESOURCE_ATTRIBUTES(department, cost_center) - Admin-configurable via managed settings (cannot be overridden by users)
- Privacy: prompt content and MCP/skill names redacted by default
Deployment Options
Five deployment paths with different cost tracking and enterprise features:
| Option | Billing | Auth | Cost Tracking | Enterprise Features |
|---|---|---|---|---|
| Teams/Enterprise | $150/seat or custom | SSO | Usage dashboard | Team mgmt, SSO, monitoring |
| Console | PAYG | API key | Usage dashboard | None |
| Bedrock | PAYG via AWS | AWS creds | AWS Cost Explorer | IAM, CloudTrail |
| Vertex | PAYG via GCP | GCP creds | GCP Billing | IAM, Audit Logs |
| Foundry | PAYG via Azure | Entra ID | Azure Cost Mgmt | RBAC, Azure Monitor |
Enterprise proxy support: HTTPS_PROXY for corporate proxy, ANTHROPIC_BASE_URL/ANTHROPIC_BEDROCK_BASE_URL/ANTHROPIC_VERTEX_BASE_URL for LLM gateways. Model pinning via ANTHROPIC_DEFAULT_OPUS_MODEL/ANTHROPIC_DEFAULT_SONNET_MODEL/ANTHROPIC_DEFAULT_HAIKU_MODEL.
Comparison with OpenCode
Section titled “Comparison with OpenCode”| Dimension | OpenCode | Claude Code |
|---|---|---|
| Architecture | Client-server (Hono HTTP + SSE) | Host-process (CLI embedded in surfaces) |
| Multi-frontend | Multiple TUIs attach via HTTP | IDE extensions host CLI as child process |
| Remote access | opencode attach <url> | Cloud VMs + session mobility commands |
| Event streaming | SSE on /event endpoint | Integration protocol (IDE) or cloud sync (web) |
| API surface | 100+ REST endpoints, OpenAPI spec | CLI flags + Agent SDK (Python/TypeScript) |
| Multi-project | Directory resolver middleware | Separate sessions per project, multi-repo in cloud |
| Authentication | Optional basic auth | OAuth (subscription) or API key, OIDC for cloud providers |
Pitfalls & Hard Lessons
Section titled “Pitfalls & Hard Lessons”Single-Process Is Good Enough for Most Cases
Section titled “Single-Process Is Good Enough for Most Cases”Both Aider and Codex ship as single-process tools and serve millions of users. The client-server split adds significant complexity (HTTP routing, serialization, event streaming, process lifecycle, authentication) that is only justified if you need multiple frontends, remote access, or process isolation.
HTTP Streaming Is Not the Same as SSE
Section titled “HTTP Streaming Is Not the Same as SSE”OpenCode’s POST /session/:sessionID/message uses Hono’s stream(), which holds the connection open and writes a single JSON response at the end. This is NOT SSE — there are no intermediate events on this endpoint. Real-time deltas arrive on a separate SSE stream (GET /event). This two-channel pattern (one for request-response, one for events) is common but means the client must correlate events with the original request.
Port Negotiation Matters
Section titled “Port Negotiation Matters”OpenCode’s try-4096-then-fallback pattern is pragmatic but creates issues when multiple instances start simultaneously. There is no service discovery or port reservation mechanism. The mDNS support partially addresses this for LAN scenarios.
Basic Auth Is Not Real Security
Section titled “Basic Auth Is Not Real Security”OpenCode’s optional basic auth (OPENCODE_SERVER_PASSWORD) is a deterrent, not a security boundary. There are no user roles, no session tokens, and no CSRF protection. The server trusts whatever directory the client claims. For a local development tool this is acceptable; for a multi-user deployment it would need a complete auth overhaul.
The Catch-All Proxy Is Surprising
Section titled “The Catch-All Proxy Is Surprising”OpenCode’s catch-all route that proxies unmatched paths to https://app.opencode.ai means the server silently forwards unknown requests to an external service. This is convenient for serving the web UI but could leak request information if the user is unaware.
Event Bus Memory Grows with Subscribers
Section titled “Event Bus Memory Grows with Subscribers”OpenCode’s Bus stores subscriptions in a Map<type, callback[]> with no subscription limits. A misbehaving client that opens many SSE connections without closing them will accumulate subscriptions. The onAbort cleanup depends on the client properly disconnecting.
WebSocket Is Only for PTY
Section titled “WebSocket Is Only for PTY”Despite having a WebSocket-capable server (Bun), OpenCode only uses WebSocket for terminal I/O. All other real-time communication uses SSE. This is a reasonable choice — SSE is simpler, unidirectional, and sufficient for event streaming.
Host-Process Model Has IDE Coupling
Section titled “Host-Process Model Has IDE Coupling”Claude Code’s approach of embedding the CLI in IDE extensions means the extension must manage the CLI process lifecycle (spawn, restart on crash, version updates). If the CLI changes its stdout format or integration protocol, all extensions break simultaneously. OpenCode’s HTTP API provides a more stable contract between frontend and backend. The trade-off is that the host-process model has zero latency overhead and no serialization cost.
Cloud Execution Requires Infrastructure
Section titled “Cloud Execution Requires Infrastructure”Claude Code’s web/cloud execution is not just “run the CLI in a VM.” It requires VM orchestration, git proxy with credential scoping, network security proxy with domain allowlisting, session persistence, and cross-surface session mobility. This is a platform capability, not something a CLI tool can offer without significant infrastructure investment.
Session Mobility Is Lossy
Section titled “Session Mobility Is Lossy”Claude Code’s session transfer between surfaces (CLI -> web, web -> CLI, CLI -> Desktop) creates new sessions in most cases. Only IDE <-> CLI shares actual conversation history. This means session state (tool approvals, file modification tracking, compaction state) is partially or fully lost during transitions.
Server-Managed Settings Are Client-Side Controls
Section titled “Server-Managed Settings Are Client-Side Controls”Server-managed settings provide centralized policy but are enforced client-side. Users with admin/sudo access on unmanaged devices can modify the binary, cached settings file, or network config. The tampered cache reverts on next server fetch, but there’s a window of non-enforcement. For stronger guarantees, use endpoint-managed settings on MDM-enrolled devices.
Third-Party Providers Lose Governance Features
Section titled “Third-Party Providers Lose Governance Features”When using Bedrock, Vertex, or Foundry, several governance features are unavailable: server-managed settings, Anthropic analytics dashboards, Claude Code Usage Report API, and Claude Code-specific workspace controls. Cost tracking falls back to the cloud provider’s native billing. Organizations need LiteLLM or similar proxy for per-key spend tracking.
OpenOxide Blueprint
Section titled “OpenOxide Blueprint”Architecture Decision: Optional Client-Server
Section titled “Architecture Decision: Optional Client-Server”OpenOxide will support both modes:
-
Single-process mode (default) — TUI and agent run in one binary, communicating via async channels. This is the Codex model and will be the primary development target.
-
Server mode (
openoxide serve) — HTTP server that exposes the agent via REST + SSE. Enables web UI, IDE extensions, and remote access.
The key insight from the reference implementations: start single-process, add the server as a layer on top. The agent core should not know whether it is talking to a local TUI or a remote HTTP client.
Crate Architecture
Section titled “Crate Architecture”openoxide-core — Agent loop, session management, tool dispatchopenoxide-tui — ratatui TUI, depends on openoxide-coreopenoxide-server — HTTP server layer, depends on openoxide-coreopenoxide-cli — CLI entry point, selects modeopenoxide-core communicates via a trait EventSink that the TUI and server both implement. The TUI calls openoxide-core directly; the server wraps it in HTTP handlers.
Server Framework
Section titled “Server Framework”Use axum (tokio-native, tower middleware, proven in production Rust services). Route structure mirrors OpenCode’s but with Rust type safety:
pub fn router(state: AppState) -> Router { Router::new() .nest("/session", session::routes()) .nest("/permission", permission::routes()) .nest("/mcp", mcp::routes()) .nest("/event", event::routes()) .nest("/pty", pty::routes()) .layer(middleware::from_fn(directory_resolver)) .layer(middleware::from_fn(auth_layer)) .with_state(state)}Event Streaming
Section titled “Event Streaming”Use axum::response::Sse with tokio::sync::broadcast for the event bus. Each directory gets its own broadcast channel. SSE heartbeat at 10-second intervals (matching OpenCode’s cadence).
Authentication
Section titled “Authentication”Optional bearer token auth (not basic auth). Token set via OPENOXIDE_SERVER_TOKEN env var. No user/role system — same trust model as OpenCode but with a more standard auth header.
Key Design Decisions
Section titled “Key Design Decisions”- Agent core is transport-agnostic. The
EventSinktrait abstracts whether events go to a local TUI or an HTTP SSE stream. - No web UI proxy. OpenOxide will not proxy unknown paths to an external service. The web UI, if built, will be a separate static site.
- WebSocket for PTY only. Follow OpenCode’s pattern — SSE for events, WebSocket only for bidirectional terminal I/O.
- Directory isolation via tower middleware. Each request extracts a directory and creates an
Instancescope, matching OpenCode’s pattern but using tower’sExtensionlayer. - OpenAPI spec generation. Use
utoipafor compile-time OpenAPI generation, enabling SDK codegen for TypeScript/Python clients. - Agent core as library crate. Following Claude Code’s Agent SDK pattern,
openoxide-coreshould be designed as a reusable library first, with the CLI and server as consumers. This enables future Python/TypeScript bindings and programmatic integration. - IDE integration via server mode. Rather than requiring IDE extensions to host the binary as a child process, OpenOxide’s planned
openoxide servemode provides the integration surface. Extensions connect via HTTP/SSE (already designed above), getting a stable API contract. The/ideconnection pattern can be supported as a convenience for external terminal sessions. - Non-interactive mode with structured output. The CLI should support
-pmode with--output-format text|json|stream-jsonand--json-schemafor structured outputs. Session IDs in JSON output enable continuation. Exit codes follow Unix conventions (0 success, 1 error, 124 timeout). - Container-deployable agent core. Design
openoxide-coreto run in containers without assuming local filesystem access patterns. This enables CI/CD integration (install binary, run-p) and potential cloud deployment. No hard dependency on local git credentials or shell profile. - Layered governance stack. OpenOxide should support a four-level governance model: developer (model/cost config), team admin (workspace limits, analytics), org admin (managed settings, permission policies, OTel config), and IT/security (MDM-deployed settings, proxy/gateway). The managed settings file at the system level (
/etc/openoxide/settings.tomlon Linux,/Library/Application Support/OpenOxide/settings.tomlon macOS) has highest precedence and cannot be overridden by user settings. - OpenTelemetry as optional crate. Create
openoxide-telemetryas an optional dependency that exports metrics and events via the OTel SDK. Metric names should follow theopenoxide.*namespace but mirror Claude Code’s metric structure for compatibility with existing dashboards. Support otlp, prometheus, and console exporters.
Crates
Section titled “Crates”| Crate | Purpose |
|---|---|
axum | HTTP framework |
tokio | Async runtime |
tower | Middleware |
tokio-tungstenite | WebSocket for PTY |
utoipa | OpenAPI generation |
serde / serde_json | Serialization |