What is OpenCode?
OpenCode is an open-source AI coding agent that runs in the terminal. It reads your codebase, writes code, runs shell commands, searches the web, and reasons about problems. You pick the AI provider and model. You run it locally with your own API keys.
It is MIT licensed. The code is at github.com/anomalyco/opencode.
History
OpenCode started as a Go project built with Bubble Tea by Kujtim Hoxha. Dax Raad and the team at Anomaly (formerly Charm/Crush) rewrote it in TypeScript. The rewrite replaced the original architecture with a client-server model, event-sourcing persistence, a plugin system, and support for multiple frontends.
TypeScript was chosen for access to the Vercel AI SDK (provider abstraction), SolidJS (reactive terminal rendering), and Hono (HTTP server). It also enabled web and desktop frontends to share code with the core.
Key Numbers
What Makes It Different
- Provider-agnostic. Works with 75+ AI providers: Anthropic, OpenAI, Google, AWS Bedrock, Azure, Ollama, local models, and more. You can switch models mid-conversation.
- Open source. MIT license.
- Multiple interfaces. Terminal TUI, desktop app (Tauri), Electron app, IDE extensions (VS Code, Zed, JetBrains, Neovim, Emacs), web interface, Slack bot, and a console mode.
- Extensible. MCP servers, LSP integration, plugins with hooks, custom tools, custom agents, custom commands, custom skills, ACP support.
- Event-sourced. Every message, tool call, and response is stored as an event. This enables undo/redo, session replay, and debugging.
Architecture Overview
OpenCode follows a client-server architecture. When you run opencode, a Hono HTTP server starts and a client connects to it. The terminal TUI, web interface, and desktop app are all clients. They communicate with the server over HTTP and Server-Sent Events (SSE).
Monorepo Structure
OpenCode is a monorepo managed with Bun (package manager and runtime) and Turborepo (build orchestration).
| Package | Path | Purpose |
|---|---|---|
opencode |
packages/opencode/ |
Core package: agent loop, tools, providers, persistence, config, server, TUI. This is the main package. |
@opencode-ai/app |
packages/app/ |
Web interface (SolidJS + Tailwind) |
@opencode-ai/desktop |
packages/desktop/ |
Tauri desktop application |
@opencode-ai/sdk |
packages/sdk/ |
TypeScript SDK for building custom clients |
The core opencode package at packages/opencode/ contains everything: the agent loop, tool registry, provider integrations, persistence layer, HTTP server, and TUI. It is not split into separate core/server/tui packages.
The Effect Pattern
The codebase uses the Effect library heavily. Effect provides dependency injection and structured error handling for TypeScript.
Every major subsystem is an Effect Service: the database, the file system, the permission system, the LLM providers. Services declare their dependencies explicitly. The type system enforces that all dependencies are provided at runtime.
// Effect service pattern used throughout OpenCode
export class Database extends Effect.Service<Database>() {
db: DrizzleDB
query: <T>(sql: SQL) => Effect<T>
}
// At boot, all services compose into a single layer
const appLayer = Layer.mergeAll(
DatabaseLive,
PermissionLive,
ProviderLive,
ToolRegistryLive,
)
Key Design Decisions
- Event sourcing. OpenCode stores every event (message sent, tool called, response received). Current state is derived by replaying events through projectors. This enables undo/redo and session replay.
- Pub/sub event bus. Components communicate through an event bus. The TUI subscribes to events from the server. The persistence layer subscribes to agent events. No direct coupling.
- Vercel AI SDK. A unified streaming interface across all LLM providers instead of provider-specific code.
- Client-server split. The server holds all state and logic. Clients are views. Multiple clients can connect to the same server. You can build new frontends without touching the core.
The Agent Loop
The agent loop is SessionPrompt.loop(). When you send a message, this function runs in a while loop: it builds the system prompt, streams the LLM response, executes any tool calls, and loops again. It exits when the LLM produces a response with no tool calls.
The Loop Visualized
Building the System Prompt
Every iteration assembles a system prompt from multiple sources (see Chapter 12 for full details):
- Provider-specific base prompt. Selected based on the model ID. Claude models get
anthropic.txt, GPT models getbeast.txtorgpt.txt, etc. - Environment block. Working directory, platform, date, git status, model name.
- Skills. Available skills listed in XML format.
- User instructions. From AGENTS.md, CLAUDE.md files (project-level and global).
- Structured output instruction. If applicable.
- User-provided system prompt. If set.
This assembly runs every iteration because context changes after tool execution.
Streaming the Response
LLM.stream() sends the full conversation to the provider and returns a stream of typed events. The stream produces these event types:
| Event Type | Description |
|---|---|
reasoning-start | Begins a reasoning/thinking block |
reasoning-delta | Incremental reasoning content |
reasoning-end | Ends a reasoning block |
text-start | Begins a text response |
text-delta | Incremental text content |
text-end | Ends a text response |
tool-input-start | Begins streaming tool call arguments |
tool-input-delta | Incremental tool argument JSON |
tool-input-end | Ends tool call argument streaming |
tool-call | Complete tool call with name and parsed arguments |
tool-result | Result returned from tool execution |
tool-error | Error from tool execution |
start-step | Begins a new agent loop step |
finish-step | Ends a step, includes token usage |
for await (const part of stream.fullStream) {
switch (part.type) {
case "text-delta":
currentText += part.textDelta
break
case "reasoning":
currentReasoning += part.textDelta
break
case "tool-call":
toolCalls.push({
name: part.toolName,
args: part.args,
})
break
}
}
Tool Execution
After the stream completes, the loop checks for tool calls. If present, each tool is executed. Results are appended to the conversation as tool result messages, and the loop iterates again.
If no tool calls were made, the loop exits and returns the response.
Subtask Handling
When the task tool is called, a child session is spawned with its own agent loop. The parent agent can fire off multiple tasks in parallel and continue working. Each subtask runs as a full agent with its own tool access (determined by the sub-agent definition; see Appendix A). Task results are returned to the parent as tool results.
Doom Loop Detection
If the LLM makes the same tool call three times in a row (same tool, same arguments), the loop aborts. This prevents burning tokens when the LLM gets stuck retrying a failing operation.
Step Tracking and Usage
Each loop iteration is a "step." OpenCode tracks step count and token usage (input, output, cache hits) across all steps. This data is exposed in the UI.
Auto-Title Generation
After the first response in a new session, a background LLM call generates a short title (50 characters or fewer) using the title agent. This title appears in the session list.
Tool System
Tools are how the LLM interacts with the filesystem, shell, and external services. Without tools it is a chatbot. With tools it can read files, write code, run tests, search the web, and spawn sub-agents.
How Tools Are Defined
Every tool is created with Tool.define(). Each definition specifies a name, description, Zod input schema, execute function, and permission requirements.
Tool descriptions are loaded from .txt files alongside the tool source code. These text files support template variables that get interpolated at runtime (e.g., the current working directory, OS platform). This keeps long descriptions out of the TypeScript code and makes them easy to edit.
export const ReadTool = Tool.define({
name: "read",
description: "Read the contents of a file",
parameters: z.object({
filePath: z.string().describe("Absolute path to the file"),
offset: z.number().optional(),
limit: z.number().optional(),
}),
async execute({ filePath, offset, limit }) {
const content = await fs.readFile(filePath, "utf-8")
// apply offset/limit, return content
return { content }
},
})
Built-in Tools
| Tool | Description | Permission |
|---|---|---|
bash |
Execute shell commands in the project directory | Ask |
read |
Read file contents with optional line range | Allow |
write |
Create or overwrite files | Ask |
edit |
Targeted find-and-replace edits in files | Ask |
apply_patch |
Apply a unified diff patch to a file. Used instead of edit/write for GPT models. | Ask |
glob |
Find files matching a glob pattern | Allow |
grep |
Search file contents with regex | Allow |
list |
List files in a directory | Allow |
task |
Spawn a sub-agent for parallel work | Allow |
webfetch |
Fetch and extract content from a URL | Ask |
websearch |
Search the web and return results | Ask |
codesearch |
Semantic code search using embeddings | Allow |
todoread |
Read the current task list | Allow |
todowrite |
Create or update tasks in the task list | Allow |
lsp |
Query language servers (diagnostics, definitions, hover) | Allow |
skill |
Load a skill file's contents into the conversation | Allow |
invalid |
Returned when the LLM calls a tool that does not exist. Provides an error message. | Allow |
batch |
Execute multiple tool calls in a single batch (experimental) | Varies |
plan_exit |
Exit plan mode and return to build mode (experimental) | Allow |
Tool Filtering per Model
Not all models get the same tool set. GPT models receive apply_patch instead of edit and write, because GPT models perform better with unified diffs than with the find-and-replace style of edit. The tool set is filtered based on the model ID before being sent to the LLM.
The Permission Model
Permissions have three levels:
- Allow - Runs immediately. Used for read-only operations.
- Ask - Requires user approval. Used for write operations and external interactions.
- Deny - Blocked entirely.
Permissions are stored as a Ruleset, an array of rules. Each rule has a tool name and a glob pattern to match against the tool's arguments. Rules are evaluated in order; the first match wins.
{
"permissions": {
"bash": {
"allow": ["npm test*", "npm run lint"],
"deny": ["rm -rf *"]
}
}
}
When a tool call comes in, the permission system serializes the tool arguments into a string and matches it against the Ruleset patterns. If no rule matches, the tool's default permission level applies.
Custom Tools
Custom tools are loaded from .opencode/tool/*.{js,ts} or .opencode/tools/*.{js,ts} at startup. They are registered alongside built-in tools and appear to the LLM the same way. This is useful for project-specific workflows like deploying, running custom test suites, or calling internal APIs.
The Task Tool (Sub-agents)
The task tool spawns a child session with its own agent loop. The parent can fire off multiple tasks in parallel. Each task is a full agent with tool access defined by its agent configuration (the general sub-agent by default, or explore for read-only searches). Sub-tasks can spawn their own sub-tasks.
Provider System
OpenCode supports 75+ AI providers through an abstraction layer built on the Vercel AI SDK. Each provider has its own SDK package. The Vercel AI SDK wraps them in a unified streamText() interface. OpenCode adds a thin layer on top for token tracking, event emission, and error handling.
Provider SDK Packages
These are the SDK packages OpenCode uses:
| Provider | SDK Package |
|---|---|
| Anthropic | @ai-sdk/anthropic |
| OpenAI | @ai-sdk/openai |
| Google (Vertex / AI Studio) | @ai-sdk/google |
| AWS Bedrock | @ai-sdk/amazon-bedrock |
| Azure OpenAI | @ai-sdk/azure |
| xAI (Grok) | @ai-sdk/xai |
| Mistral | @ai-sdk/mistral |
| Groq | @ai-sdk/groq |
| OpenRouter | @openrouter/ai-sdk-provider |
| DeepInfra | @ai-sdk/deepinfra |
| Cerebras | @ai-sdk/cerebras |
| Cohere | @ai-sdk/cohere |
| Together AI | @ai-sdk/togetherai |
| Perplexity | @ai-sdk/perplexity |
| Vercel | @ai-sdk/vercel |
| GitLab | gitlab-ai-provider |
| GitHub Copilot | Custom Copilot SDK |
| Ollama / OpenAI-compatible | @ai-sdk/openai (with custom baseURL) |
Model Catalog
OpenCode maintains a model catalog sourced from models.dev. It stores: context window size, tool use support, extended thinking support, pricing per token, and max output tokens. This metadata drives context management, auto-compact thresholds, and cost display.
LLM.stream() Internals
When the agent loop calls LLM.stream():
- The provider and model are resolved from config.
- The model is wrapped with
wrapLanguageModelmiddleware. This middleware layer handles token tracking, event emission, and provider-specific adjustments. - For LiteLLM proxy compatibility, a dummy
_nooptool is injected. Some proxies fail when no tools are provided, so this ensures the tools array is never empty. streamText()from the Vercel AI SDK is called with the wrapped model, system prompt, messages, and tool schemas.- The resulting stream and usage metadata are returned.
async function stream(options) {
const provider = resolveProvider(options.model)
const wrapped = wrapLanguageModel(provider, middleware)
const result = await streamText({
model: wrapped,
system: options.systemPrompt,
messages: options.messages,
tools: options.toolSchemas,
maxTokens: options.maxTokens,
})
return {
fullStream: result.fullStream,
usage: result.usage,
}
}
Provider-Specific Handling
- Anthropic gets beta headers for prompt caching and extended thinking. The system prompt uses
anthropic.txtwhich is optimized for Claude. - OpenAI supports both Chat Completions API and Responses API.
- Google Gemini can return multiple tool calls in a single message chunk.
- Local models via Ollama use the OpenAI-compatible SDK with a custom
baseURL.
You can switch models mid-conversation. The conversation history is serialized and continues with the new model.
The Server & API
OpenCode runs a Hono HTTP server on a local port. All clients -- including the built-in TUI -- communicate with this server. This is what enables multiple frontends to connect to the same running instance.
Server Configuration
- CORS: Allows
localhost,tauri://, and*.opencode.aiorigins. - Auth: Optional basic auth via the
OPENCODE_SERVER_PASSWORDenvironment variable. - Compression: Gzip compression enabled.
- Instance bootstrapping: A middleware initializes the project instance (database, config, tools) before handling requests.
API Routes
The full route table is in Appendix C. Here is the high-level grouping:
SSE Event Streaming
Clients connect to GET /event for a Server-Sent Events stream scoped to the current session. GET /global/event provides a global stream across all sessions. Events pushed down this stream include:
message.updated- A message part was updated (text streaming in, tool result arrived)part.updated,part.delta- Part-level updates and incremental contentsession.created,session.updated- Session lifecyclepermission.asked- A tool needs user approval
The TUI, web interface, and desktop app all subscribe to the same event stream. All frontends stay in sync.
Multi-Instance Support
Each project directory gets its own server on a different port, with its own database and session list. The port is determined by hashing the project directory path, so reopening the same project always gets the same port.
The SDK Client
The @opencode-ai/sdk package provides a TypeScript client for the server API. It supports two transport modes:
- HTTP transport. For external clients connecting over the network.
- In-process transport. For the TUI, which runs in the same process as the server. Skips the network layer.
import { OpenCode } from "@opencode-ai/sdk"
const client = new OpenCode({ url: "http://localhost:3000" })
const session = await client.session.create()
await client.message.create(session.id, {
content: "Refactor the auth module",
})
client.on("message.updated", (msg) => {
console.log(msg.content)
})
The Terminal UI
The terminal UI is the primary interface for OpenCode. It is built on OpenTUI, a custom terminal rendering framework that uses SolidJS for reactive updates.
OpenTUI
OpenTUI uses SolidJS instead of a virtual DOM. When state changes, SolidJS updates only the terminal cells that depend on that state. This avoids full re-renders.
OpenTUI provides:
- A component model (JSX components that render to terminal cells)
- Flexbox-like layout for positioning
- Reactive state via SolidJS signals
- Keyboard event handling
- Text wrapping, scrolling, and ANSI color support
Component List
The TUI is composed of these dialog and UI components:
| Component | Purpose |
|---|---|
dialog-agent | Agent/mode selection dialog |
dialog-command | Command palette (fuzzy-searchable) |
dialog-mcp | MCP server management |
dialog-model | Model picker |
dialog-provider | Provider configuration |
dialog-session-list | Session browser |
dialog-skill | Skill selection and preview |
dialog-status | Status information display |
dialog-theme-list | Theme picker |
dialog-workspace-list | Workspace browser |
prompt | Input prompt area with autocomplete |
Provider Architecture
The TUI uses a provider pattern (like React context) to inject services into the component tree:
| Provider | What It Does |
|---|---|
| Theme | Colors, styling, visual configuration |
| SDK | Connection to the OpenCode server |
| Sync | Real-time state synchronization via SSE events |
| Dialog | Modal dialog management |
| Command | Command palette and slash command registry |
| Keybind | Keyboard shortcut management |
How the TUI Connects to the Server
The TUI connects via the in-process SDK transport. It subscribes to the SSE event stream and maintains a local reactive store. When you type a message, it sends POST /session/:id/prompt. SSE events stream in and update the UI in real-time: text appears incrementally, tool calls show progress, permission requests appear as dialogs.
Build and Plan Modes
Switch between modes with Tab:
- Build mode. Full tool access. Can read, write, edit files, run commands, spawn tasks.
- Plan mode. Read-only. Can read files and search, but cannot modify anything. Can only edit plan files.
The Command Palette
Press Ctrl+K / Cmd+K to open the command palette. It provides fuzzy-searchable access to: model switching, agent selection, session management, configuration, and all registered commands.
Data & Persistence
OpenCode stores all state in SQLite via Drizzle ORM. The database lives at ~/.local/share/opencode/opencode.db (or opencode-{channel}.db for non-default channels). SQLite is configured with WAL mode, NORMAL sync, 5-second busy timeout, 64MB cache, and foreign keys enabled.
The persistence layer uses event sourcing: instead of storing current state directly, it stores every event that occurred. Current state is computed by replaying events through projector functions that materialize the data into relational tables.
Database Schema
There are 8 tables. The three core tables (session, message, part) store conversation data. The message and part tables use a data JSON column for type-specific fields, with only the IDs and foreign keys as dedicated columns.
| Table | Columns | Purpose |
|---|---|---|
session |
id, project_id (FK), workspace_id, parent_id, slug, directory, title, version, share_url, summary_additions, summary_deletions, summary_files, summary_diffs (JSON), revert (JSON), permission (JSON Ruleset), time_created, time_updated, time_compacting, time_archived | Each conversation. parent_id links subtask sessions to their parent. revert stores undo state. |
message |
id, session_id (FK, cascade), time_created, time_updated, data (JSON) | Messages in a session. The data JSON contains: role (user/assistant), tokens, cost, error, parentID (links assistant replies to user messages), modelID, finish reason, summary flag. |
part |
id, message_id (FK, cascade), session_id, time_created, time_updated, data (JSON) | Typed parts within a message. The data JSON contains the part type and all type-specific fields. |
todo |
session_id, content, status, priority, position | Task list items. Composite PK: (session_id, position). |
permission |
project_id (PK), data (JSON Ruleset), time_created, time_updated | Per-project permission rules. |
project |
id, name, worktree | Registered projects. |
event_sequence |
aggregate_id (PK), seq | Tracks the latest sequence number per aggregate (session). |
event |
id, aggregate_id (FK, cascade), seq, type, data (JSON) | The event log. Every state change recorded as a typed event. |
Messages and Parts
The core data model is: a session contains messages, and each message contains parts. A single assistant message typically contains multiple parts — some text, possibly reasoning, and one or more tool calls.
Messages carry metadata in their data JSON: the role, token counts, cost, error state, the model ID used, finish reason, and a parentID field that links assistant messages back to the user message that triggered them. Assistant messages also have a summary boolean flag (true for compaction summary messages) and a finish flag (true when the response is complete).
The Part Type System
Every part has a base of { id, sessionID, messageID } plus type-specific fields:
| Part Type | Fields | Description |
|---|---|---|
TextPart |
text, synthetic?, ignored?, time, metadata? |
Plain text from the LLM. synthetic marks injected text (e.g. mode-switch reminders). ignored excludes it from LLM conversion. |
ReasoningPart |
text, time, metadata? |
Extended thinking/reasoning content (e.g. Claude's thinking blocks). |
ToolPart |
callID, tool, state, metadata? |
A tool invocation. The state field is a discriminated union that transitions through a state machine (see below). |
FilePart |
mime, filename?, url, source? |
File attachment. Source can be a file path, an LSP symbol reference, or an MCP resource. |
CompactionPart |
auto, overflow? |
Marks a compaction boundary on a user message. Messages before this are hidden when loading conversation for the LLM. |
SubtaskPart |
prompt, description, agent, model?, command? |
A pending subtask to be executed by the task tool. |
SnapshotPart |
snapshot |
Git tree hash representing filesystem state at a point in time. |
PatchPart |
hash, files[] |
Records which files changed between two snapshots. hash is the "before" tree hash. Used for revert. |
AgentPart |
name, source? |
An agent mention/reference. |
RetryPart |
attempt, error |
Records an API retry. Error includes statusCode, isRetryable, responseHeaders. |
StepStartPart |
snapshot? |
Marks the beginning of an agent loop step. Snapshot is the git tree hash at step start. |
StepFinishPart |
reason, snapshot?, cost, tokens |
Marks end of a step. Tokens includes: input, output, reasoning, cache.read, cache.write. |
ToolPart State Machine
The state field on a ToolPart is a discriminated union on status that transitions through four states:
State fields by status:
- pending:
input: {},raw: ""— created attool-input-startstream event. Raw input string accumulates viatool-input-delta. - running:
input(parsed args),title?,metadata?,time.start— transitions attool-callevent when input is fully parsed. - completed:
input,output,title,metadata,time.start,time.end,time.compacted?,attachments?— transitions attool-result. Thetime.compactedfield is set later during pruning. - error:
input,error(string),metadata?,time.start,time.end— transitions attool-error, or when the stream ends with tools still in pending/running state (set to "Tool execution aborted").
How Parts Are Created During Streaming
SessionProcessor.create() iterates over the LLM stream and creates/updates parts in real time:
reasoning-start→ creates aReasoningPartwith empty text andtime.start = Date.now()reasoning-delta→ appends text, publishes delta event to the bus for UI updatesreasoning-end→ trims text, setstime.endtext-start→ creates aTextPartwith empty texttext-delta→ appends text, publishes deltatext-end→ trims text, runs plugin hookexperimental.text.complete, setstime.endtool-input-start→ createsToolPartinpendingstatetool-call→ transitions torunning, checks for doom loop (3 identical calls triggers permission ask)tool-result→ transitions tocompletedwith output, title, metadata, attachmentstool-error→ transitions toerrorstart-step→ callsSnapshot.track()to capture filesystem state, createsStepStartPartfinish-step→ computes token usage and cost, createsStepFinishPart, createsPatchPartif files changed since the step's snapshot
All part creation and updates go through Session.updatePart(), which calls SyncEvent.run(MessageV2.Event.PartUpdated, ...). This persists the event and runs the projector to upsert the part row.
Converting Messages to LLM Format
MessageV2.toModelMessages() converts stored messages and parts into the format expected by the Vercel AI SDK:
- User messages: TextParts become
{ type: "text", text }. FileParts become{ type: "file", url, mediaType }. CompactionParts become{ type: "text", text: "What did we do so far?" }. - Assistant messages: TextParts and ReasoningParts map directly. Completed ToolParts include the output (or
"[Old tool result content cleared]"iftime.compactedis set). Error ToolParts include the error text. Pending/running ToolParts become"[Tool execution was interrupted]". - Skipped messages: Assistant messages with errors are skipped entirely, unless the error is an
AbortedErrorand the message has substantive parts.
For providers that don't support media in tool results (anything other than Anthropic, OpenAI, Bedrock, Vertex, or Gemini 3+), media attachments are extracted from tool results and injected as a separate user message after the assistant message.
Loading Messages: filterCompacted
MessageV2.filterCompacted() loads messages for the LLM, respecting compaction boundaries. It iterates messages in reverse chronological order (newest first). When it finds an assistant message that is a completed summary (summary: true, has finish, no error), it records that message's parentID. When it then reaches the corresponding user message and that message has a CompactionPart, it stops. Everything older is excluded.
The result is: the compaction user message + its summary response + all subsequent messages. The summary replaces all prior conversation history.
Event Sourcing
Events are defined with SyncEvent.define({ type, version, aggregate, schema }). The aggregate field names the key used as the aggregate ID (always "sessionID" for session events).
Seven event types exist:
| Event Type | Data | Projector Action |
|---|---|---|
session.created |
sessionID, info (full Session.Info) | INSERT into session table |
session.updated |
sessionID, info (partial Session.Info) | UPDATE session row (only changed fields) |
session.deleted |
sessionID, info | DELETE session row (cascades to messages, parts) |
message.updated |
sessionID, info (MessageV2.Info) | UPSERT into message table (insert or update on conflict) |
message.removed |
sessionID, messageID | DELETE message row |
message.part.updated |
sessionID, part (full Part), time | UPSERT into part table |
message.part.removed |
sessionID, messageID, partID | DELETE part row |
SyncEvent.run() executes within an immediate SQLite transaction: reads the current sequence number for the aggregate, increments it, runs the projector (which does the actual INSERT/UPDATE/DELETE), optionally persists the event to the event table (when workspace sync is enabled), then emits the event on the bus after commit.
Events can be replayed via SyncEvent.replay() for workspace sync. Replay checks sequence ordering and silently skips already-applied events (idempotent).
Fork, Revert, and Undo
Fork (Session.fork()) creates a new session and copies messages up to a specified point. Each message and part gets a new ID. The parentID links on assistant messages are remapped to the new message IDs.
Revert (SessionRevert.revert()) restores files to a previous state without deleting messages:
- Walks all messages to find the revert target (by messageID and optional partID)
- Collects all
PatchParts that occur after the target - Takes a snapshot of current state
- For each file in each patch: checks out the file from the patch's git tree hash, or deletes it if it didn't exist at that point
- Stores revert metadata on the session:
{ messageID, partID?, snapshot?, diff? }
Cleanup happens when the user sends the next message after a revert. Messages after the revert point are deleted via SyncEvent.run(MessageV2.Event.Removed). If the revert was mid-message, parts from the revert point onward are also deleted.
Unrevert (SessionRevert.unrevert()) restores the full git tree to what it was before the revert, then clears the revert field.
Snapshots: The Shadow Git Repo
OpenCode maintains a shadow Git repository separate from your working repo. It lives at <data-dir>/snapshot/<projectID>/<hash(worktree)>/. All operations are serialized per directory using a semaphore.
Operations:
init()— creates the git directory withautocrlf=false,longpaths=true,symlinks=true,fsmonitor=false.track()— finds changed/new files viagit diff-filesandgit ls-files --others, filters out files over 2MB, runsgit addandgit write-tree. Returns the tree hash.patch(hash)— stages current state, diffs against the given tree hash, returns the list of changed files.restore(snapshot)— runsgit read-tree+git checkout-index -a -fto restore the full working tree.revert(patches)— selective per-file revert:git checkout <hash> -- <file>for each file, or deletes if the file didn't exist.diff(hash)/diffFull(from, to)— computes diffs between tree hashes, returning structuredFileDiff[]with before/after content.cleanup()— runsgit gc --prune=7.days, scheduled hourly.
Context Window Management
Every LLM has a context window limit. In a long coding session with many tool calls, you can hit it fast. OpenCode manages this automatically with a two-phase system.
Overflow Detection
Before each loop iteration, OpenCode checks whether the context is approaching the limit. The formula:
overflow = totalTokens >= inputLimit - reserved
// reserved = min(20_000, maxOutputTokens)
inputLimit is the model's context window size minus max output tokens. reserved defaults to the smaller of 20,000 tokens or maxOutputTokens. If totalTokens exceeds this threshold, the two-phase cleanup runs.
Token Thresholds
Two constants control pruning behavior:
- PRUNE_PROTECT = 40,000 tokens. The most recent 40K tokens of conversation are never pruned.
- PRUNE_MINIMUM = 20,000 tokens. A tool output must be at least 20K tokens old (measured from the end of conversation) before it is eligible for pruning.
The Two-Phase Approach
Phase 1: Pruning
Pruning does not call the LLM. It finds old tool call results (older than PRUNE_MINIMUM tokens from the end of conversation) and replaces their output with a placeholder like [output pruned]. The tool call record stays in the history so the LLM knows a tool was used, but the large result body is removed.
The most recent PRUNE_PROTECT tokens (40K) are never pruned.
Phase 2: Compaction
If pruning is not enough, the older portion of the conversation is sent to the compaction agent. This agent produces a summary that captures the goal, key decisions, discoveries, and current state. The summary is inserted as a CompactionPart at the beginning, and the old messages it summarizes are removed.
The compaction summary follows a structured template (see Appendix B).
Plugin Hook
The experimental.session.compacting plugin hook fires before compaction. Plugins can use this to customize compaction behavior or inject additional context into the summary.
Extensibility
OpenCode is designed to be extended at every level: custom tools, custom agents, custom commands, plugins with hooks, MCP servers, LSP integration, and ACP support.
MCP (Model Context Protocol)
OpenCode has first-class MCP support. Three transport types are supported:
- StdioClientTransport. Runs a local process. Communicates over stdin/stdout.
- StreamableHTTPClientTransport. Connects to a remote HTTP server with streaming.
- SSEClientTransport. Connects to a remote server using Server-Sent Events.
MCP servers that require authorization are handled via OAuth (RFC 7591 dynamic client registration).
{
"mcp": {
"servers": {
"database": {
"type": "stdio",
"command": "npx",
"args": ["@modelcontextprotocol/server-postgres"]
},
"github": {
"type": "sse",
"url": "https://mcp.github.com/sse"
}
}
}
}
Once connected, MCP tools appear alongside built-in tools. The LLM uses them the same way.
LSP Integration
OpenCode auto-detects language servers (tsserver, pyright, gopls, etc.) and connects to them. The agent gets access to diagnostics, hover information, go-to-definition, and symbol search. LSP diagnostics are injected into the system prompt so the agent knows about current errors.
Plugins
Plugins hook into lifecycle events. The full list of hooks:
| Hook | When It Fires |
|---|---|
chat.params | Before an LLM call. Can modify system prompt, parameters. |
chat.headers | Before an LLM HTTP request. Can add/modify headers. |
tool.definition | When tool definitions are assembled. Can modify tool schemas. |
tool.execute.before | Before a tool runs. Can validate or modify arguments. |
tool.execute.after | After a tool runs. Can post-process results. |
experimental.chat.system.transform | Transform the system prompt before sending. |
experimental.chat.messages.transform | Transform the message array before sending. |
experimental.text.complete | Text completion hook for autocomplete. |
experimental.session.compacting | Before compaction runs. |
Built-in Plugins
OpenCode ships with authentication plugins for specific providers:
- CodexAuthPlugin - Handles OpenAI Codex authentication
- CopilotAuthPlugin - Handles GitHub Copilot OAuth flow
- GitlabAuthPlugin - Handles GitLab authentication
- PoeAuthPlugin - Handles Poe authentication
Custom Commands
Markdown files in .opencode/command/**/*.md or .opencode/commands/**/*.md register as slash commands. The file's content becomes the prompt sent to the agent when the command is invoked. Commands appear in the command palette.
Custom Agents
Markdown files in .opencode/agent/**/*.md or .opencode/agents/**/*.md define custom agents. Each file specifies the agent's system prompt, tool access, and behavior. Custom agents appear alongside the built-in build and plan agents.
Custom Tools
JavaScript or TypeScript files in .opencode/tool/*.{js,ts} or .opencode/tools/*.{js,ts} are loaded at startup and registered as tools.
IDE Extensions
OpenCode has extensions for VS Code, Zed, JetBrains, Neovim, and Emacs. These connect to the same OpenCode server and share sessions, tools, and config.
The VS Code extension is installed via code --install-extension sst-dev.opencode. OpenCode detects IDE environments through the TERM_PROGRAM and GIT_ASKPASS environment variables.
ACP (Agent Client Protocol)
ACP allows agents to communicate with each other. OpenCode can be orchestrated by a larger system or orchestrate other ACP-compatible agents.
Desktop & Beyond
The client-server architecture means OpenCode can run anywhere a browser or native app can.
Tauri Desktop App
The primary desktop app uses Tauri (Rust-based). It wraps the web interface in a native window.
- Starts the OpenCode server as a child process
- Opens a native window with the web UI
- The web UI connects to the local server over HTTP + SSE
- On close, it shuts down the server
Provides native OS integration: system notifications, file dialogs, menu bar, keyboard shortcuts.
Electron Desktop App
An Electron-based desktop app is also available as an alternative to Tauri. It provides the same web UI in an Electron shell.
Web Interface
The web interface is a SolidJS + Tailwind application in the @opencode-ai/app package. It provides markdown rendering, syntax-highlighted code diffs, file tree views, and works with any OpenCode server.
Slack Integration
A Slack bot connects to an OpenCode server. You message the bot in a channel or DM, and it responds with full agent capabilities including tool use and streaming. Useful for team collaboration -- everyone in the channel sees the agent work in real-time.
Console App
A stripped-down console mode for environments where the full TUI is not available (CI/CD pipelines, basic SSH sessions). Works with plain text input/output.
IDE Extensions
Extensions for VS Code, Zed, JetBrains, Neovim, and Emacs. They are alternative frontends that connect to the same server.
VS Code: code --install-extension sst-dev.opencode. OpenCode detects IDE environments via TERM_PROGRAM and GIT_ASKPASS environment variables to adjust behavior (e.g., opening files in the editor instead of printing contents).
The SDK
Everything above is built on the @opencode-ai/sdk package. It provides full TypeScript types, session management, message streaming, SSE event subscription, permission handling, and configuration management. Use it to build custom frontends.
System Prompt Construction
The system prompt is assembled dynamically every iteration of the agent loop. It determines how the LLM behaves, what it knows about, and what instructions it follows. This chapter covers the exact assembly process.
Provider-Specific Base Prompts
OpenCode selects a base prompt based on the model ID. Prompt files live in src/session/prompt/:
| File | Matched When | Key Characteristics |
|---|---|---|
anthropic.txt |
Model ID contains "claude" | Opens with "You are OpenCode, the best coding agent on the planet." Covers TodoWrite task management, Task tool usage, code reference format. |
beast.txt |
Model ID contains "gpt-4", "o1", or "o3" | Aggressive autonomous behavior: "MUST iterate and keep going until the problem is solved." Requires internet research. |
codex.txt |
Model ID contains "gpt" AND "codex" | Stripped-down. Prefers apply_patch. ASCII-only output. Git hygiene rules. |
gpt.txt |
Model ID contains "gpt" (after codex check) | Commentary/final channel system. Varied personality. |
gemini.txt |
Model ID contains "gemini-" | Long prompt with core mandates, editing constraints, new application workflow. |
trinity.txt |
Model ID contains "trinity" | Copilot-style formatting. File reference rules. |
default.txt |
Fallback (no other match) | "You are opencode, an interactive CLI tool..." Emphasizes conciseness ("fewer than 4 lines"), proactiveness rules, "DO NOT ADD ANY COMMENTS". |
Selection logic checks the model ID in this order: "claude" -> "gpt-4"/"o1"/"o3" (beast) -> "gpt"+"codex" -> "gpt" -> "gemini-" -> "trinity" -> default.
Assembly Order
The final system prompt is composed in this order:
- Agent prompt (if custom) OR provider-specific base prompt. If the current agent has a custom prompt defined, that is used. Otherwise the provider-specific prompt from the table above.
- Environment block. Model name, working directory, worktree root, platform, date, git status.
- Skills listing. Available skills in XML format.
- User instructions. AGENTS.md, CLAUDE.md files from project and global paths.
- Structured output instruction. If the agent requires structured output.
- User-provided system prompt. If set in config.
Environment Block Format
You are powered by the model named {model.api.id}. The exact model ID is {providerID}/{model.api.id}
Here is some useful information about the environment you are running in:
<env>
Working directory: {Instance.directory}
Workspace root folder: {Instance.worktree}
Is directory a git repo: {yes|no}
Platform: {process.platform}
Today's date: {new Date().toDateString()}
</env>
Instruction Loading
OpenCode searches for instruction files named AGENTS.md, CLAUDE.md, and CONTEXT.md (deprecated). These are loaded from two locations:
- Project. Walks up from the current working directory to the worktree root, collecting every matching file found along the way.
- Global. Checks
~/.config/opencode/and~/.claude/.
Additionally, contextual instructions are loaded when files are read during the session. When the agent reads a file, OpenCode walks up from that file's directory looking for AGENTS.md/CLAUDE.md and injects any found instructions into the prompt.
Skills Format in Prompt
Skills are listed in the prompt using XML:
<available_skills>
<skill>
<name>{name}</name>
<description>{description}</description>
<location>{file URL}</location>
</skill>
</available_skills>
Skills are markdown files in .opencode/skills/. They are not loaded into the prompt by default -- the agent sees the listing and uses the skill tool to load one when needed.
Agent Definitions
OpenCode has seven built-in agents. Each has a mode, tool access rules, and sometimes a custom prompt.
| Agent | Mode | Description | Tool Access |
|---|---|---|---|
build |
primary | Default agent. Full tool access. This is what runs when you type a message normally. | All tools. question and plan_enter permissions allowed. |
plan |
primary | Read-only. Can only edit plan files. Used for analysis and planning without modifying the codebase. | Read-only tools. plan_exit allowed. Write tools denied. |
general |
subagent | For multi-step tasks spawned via the task tool. Full tool access minus todowrite. | All tools except todowrite (denied). |
explore |
subagent | Read-only file search specialist. Has a custom prompt optimized for finding information. | Only: grep, glob, list, bash, webfetch, websearch, codesearch, read. |
compaction |
primary (hidden) | Generates conversation summaries during context compaction. No tools. | All tools denied. |
title |
primary (hidden) | Generates conversation titles (50 chars or fewer). Temperature 0.5. No tools. | All tools denied. |
summary |
primary (hidden) | Generates PR-style summaries of conversations. No tools. | All tools denied. |
Hidden agents are not shown in the agent picker. They are used internally by the system.
Prompt Templates
Key prompt templates used by the internal agents and system features.
Compaction Prompt
The compaction agent receives a user message with this structure:
Summarize this conversation so far. Include:
**Goal**: What was the user trying to accomplish?
**Instructions**: What specific instructions or constraints did the user give?
**Discoveries**: What was learned along the way? (errors found, patterns identified, etc.)
**Accomplished**: What has been done so far?
**Relevant files**: List any files that were read, modified, or are relevant to the task.
The compaction agent's system prompt instructs it to produce a concise summary following this template. The result replaces the older messages it summarized.
Title Prompt
The title agent's system prompt:
You are a title generator. You output ONLY a thread title.
The title must be 50 characters or fewer.
No quotes. No punctuation at the end. No prefixes.
Temperature is set to 0.5 for slightly varied but focused output.
Mode-Switching Reminders
When switching between modes, reminder messages are injected:
- Plan mode active: A message reminds the agent it is in plan mode, can only read files and think, and should use
plan_exitto return to build mode. - Build switch: A message reminds the agent it has returned to build mode with full tool access.
- Max steps reached: When the step count hits the configured maximum, a message tells the agent to wrap up and provide a summary.
Mid-Loop User Messages
If the user sends a message while the agent is still processing (mid-loop), the message is wrapped in <system-reminder> tags and injected into the conversation. This allows the user to provide corrections or additional context without waiting for the agent to finish.
<system-reminder>
{user's message text}
</system-reminder>
Complete API Reference
All HTTP routes exposed by the OpenCode server.
| Method | Route | Description |
|---|---|---|
| GET | /session | List all sessions |
| POST | /session | Create a new session |
| GET | /session/:id | Get session by ID |
| DELETE | /session/:id | Delete a session |
| GET | /session/:id/message | List messages in a session |
| POST | /session/:id/message | Add a message to a session |
| POST | /session/:id/prompt | Submit a prompt (starts the agent loop) |
| POST | /session/:id/abort | Abort the running agent loop |
| POST | /session/:id/compact | Trigger manual compaction |
| POST | /session/:id/revert | Revert to a previous state (undo) |
| POST | /session/:id/fork | Fork a session at a given point |
| POST | /session/:id/share | Generate a shareable link for a session |
| GET | /event | SSE stream for session events |
| GET | /global | Global session data |
| GET | /global/event | Global SSE stream (all sessions) |
| GET | /project | Get project info |
| GET | /provider | List available providers |
| GET | /config | Get current configuration |
| PUT | /config | Update configuration |
| GET | /mcp | List MCP servers |
| POST | /mcp | Add an MCP server |
| DELETE | /mcp | Remove an MCP server |
| GET | /permission | List permission rules |
| POST | /permission | Add/update permission rules |
| GET | /question | Get pending permission questions |
| POST | /question | Answer a permission question |
| WS | /pty | WebSocket for pseudo-terminal access |
| GET | /file | Read a file |
| GET | /auth/:providerID | Get auth status for a provider |
| POST | /auth/:providerID | Initiate auth for a provider |
| DELETE | /auth/:providerID | Remove auth for a provider |
| GET | /agent | List available agents |
| GET | /skill | List available skills |
| GET | /lsp | Get LSP status and diagnostics |
| GET | /command | List available commands |
| GET | /path | Get path information |
| GET | /vcs | Get version control status |
Event Bus Events
Events published on the internal event bus. These are the events that flow through SSE to connected clients.
| Event | Description |
|---|---|
session.created | A new session was created |
session.updated | Session metadata changed (title, status, model) |
session.deleted | A session was deleted |
session.compacted | A session was compacted (older messages summarized) |
message.updated | A message was updated (new parts, content changes) |
part.updated | A message part was updated |
part.delta | Incremental content added to a part (streaming text) |
permission.asked | A tool is requesting user permission |
permission.replied | User responded to a permission request |
lsp.updated | LSP diagnostics or status changed |
mcp.tools.changed | MCP server tools were added, removed, or changed |
server.connected | A client connected to the server |
server.heartbeat | Periodic heartbeat for connection keepalive |
server.instance.disposed | The server instance is shutting down |