OpenCode in Rust — by @vakandi. The open source AI coding agent, rebuilt as a native CLI, TUI and HTTP server.

Rust port · MIT licensed

OpenCode in Rust

The open source AI coding agent, rebuilt as a native binary. Terminal UI, CLI and HTTP server — no Docker, no runtime, no container to contain.

Command
opr
Binary
opencode
Runtime
Node 18+
Container
not required
opr — session tty
$ opr
# entry command; binary is `opencode`

> audit the storage layer for schema drift

  ▸ read crates/opencode-storage/src/schema.rs
  ▸ grep  ALL_MIGRATIONS
  ▸ tool  22 built-ins + live MCP in one registry

  ✓ migrations replay on every connect
  ✓ history spilled to SQLite — sessions, messages, parts

> 
Core language: Rust 21 crates 571 tests passing No Docker, no container runtime License: MIT

01 / Install

Three surfaces, one engine

Twenty-two built-in tools and every live MCP tool land in one registry, resolved per turn. Permissions are scoped per path, last matching rule wins.

01 / NPM

Node launcher

npm install -g opencode-rust

Requires Node 18+. opr is a dispatching launcher here.

opencode-rust on npm

02 / HOMEBREW

Homebrew tap

brew install vakandi/tap/opencode-rust

Builds from source with cargo install, so the first install compiles.

03 / SOURCE

Clone & build

git clone https://github.com/vakandi/opencode-rust && ./install.sh

Fingerprint-stamped: an unchanged tree skips the rebuild. --force defeats the stamp.

BEFORE FIRST RUN

Set a provider key, for example export ANTHROPIC_API_KEY=...

SURFACE NOTE

opr is not identical across installers. Through npm it is a dispatcher, so opr --some-flag becomes opencode tui --some-flag. Through Homebrew it is a plain symlink, so opencode tui --some-flag is rejected. Both behaviours are intentional.


02 / Capabilities

What ships in the box

The original engine kept conversation history in RAM, re-derived the full tool schema on every step, and never evicted per-directory state. Docker existed to contain that leak. Five teams of five agents pinned a MacBook Pro M5 at 99% CPU — the container was the limit, not the workload.

01

Interaction modes

Five surfaces over the same engine.

  • TUI (default)
  • run
  • HTTP server
  • Web
  • ACP

02

Sessions

Full session lifecycle with lineage preserved.

  • create
  • continue
  • fork
  • import / export
  • parent & child links

03

Tool system

Built-ins and live MCP tools resolve through one registry.

  • 22 built-in tools
  • live MCP tools

04

Permissions

Rules scoped per path, evaluated in order.

  • per-path scoping
  • last matching rule wins

05

Providers

Multi-provider calls with reasoning control.

  • routing
  • reasoning-effort control
  • streaming tool calls

06

Extensibility

Bring your own processes; they join the tool registry.

  • TypeScript plugin bridge
  • MCP stdio + HTTP
  • LSP

07

Storage

Conversation state on disk, not in memory.

  • SQLite
  • sessions
  • messages
  • parts

03 / Engineering

Three defects worth reading about

Rust replaces the runtime, not the features. Conversation history spills to SQLite, request bodies stream, tool schemas are cached per model and tool-set, and instance state is pooled with LRU and TTL eviction.

run does not abandon its work

It used to make a single provider call and never execute a tool, so a spawned subagent died with the process.

It now drives the real session loop, then holds the process open until four things settle.

Drains on
the turn → session.idle → child sessions quiet → background-task ledger
Failure mode
fails open — absent db, missing file, corrupt JSON or a stale entry all mean done. Never a hang.

MCP actually works

Three separate defects, each of which silently produced a “successful” boot exposing zero tools.

Framing
wrote LSP Content-Length instead of newline-delimited JSON — every server timed out
Config
an enum whose {enabled} toggle variant shadowed the full server variant, discarding every definition
Boot
sequential and unbounded
After
3 servers in ~4s → 77 tools reaching agents

Streaming actually streams

The TUI SSE listener dropped early deltas and triggered a full transcript refetch per delta.

Before
~15 fetches per second against the transcript endpoint
After
deltas are buffered and replayed; an applied delta triggers a redraw instead of a refetch

04 / Rationale

Why this exists in Rust

The container was never the fix. Conversation history spills to SQLite, request bodies stream, tool schemas are cached per model and tool-set, and instance state is pooled with LRU and TTL eviction — so there is nothing left to contain.

TypeScript engine → Rust port
Concern TypeScript engine opencode-rust
Conversation history Held in RAM Spilled to SQLite
Tool schemas Re-derived on every LLM step Cached per (model, tool-set)
Request bodies Buffered whole Streamed
Instance state (MCP / LSP / plugins) Per-directory, unbounded, never evicted Pooled by config hash, LRU + TTL eviction
Containment A Docker runtime existed to hold the leak No container, no cgroup, no leak to contain
Heavy run, 5 teams × 5 agents Pinned a MacBook Pro M5 at 99% CPU — the machine became unusable That was the limit, not the workload

Get started

Run it locally in under a minute

Node 18+, one provider key, no container runtime.