OpenCode Architecture Index
OpenCode Architecture Index (packages/opencode/src)
Section titled “OpenCode Architecture Index (packages/opencode/src)”Reference Pin
Section titled “Reference Pin”- Repository:
references/opencode - Commit SHA:
7ed449974864361bad2c1f1405769fd2c2fcdf42 - Indexed scope:
packages/opencode/src
What This Codebase Is
Section titled “What This Codebase Is”OpenCode is a Bun + TypeScript AI agent runtime with:
- A CLI entrypoint (
src/index.ts) that registers all commands and initializes logging + DB migration. - A headless HTTP/SSE/WebSocket server (
src/server/server.ts) used by TUI, web, and attached clients. - A session engine (
src/session/*) that manages message state, tool execution, retries, snapshots, compaction, and summaries. - A dynamic provider layer (
src/provider/*) that maps config/auth/models to AI SDK language models. - First-party + plugin + MCP tool orchestration (
src/tool/*,src/mcp/*).
This repo is client-server in shape, but the core logic is mostly reusable as local modules.
Top-Level Domain Map (src/*)
Section titled “Top-Level Domain Map (src/*)”acp/: Agent Client Protocol bridge — maps OpenCode sessions/tools/permissions to external ACP clients.agent/: agent definitions, defaults, permissions, generation prompt.bun/: Bun runtime integration — package registry, dynamic SDK installs, process spawning.bus/: typed event pub/sub system with Zod schemas, instance-scoped + global cross-process bus.cli/: command implementations (run,serve,attach, etc.) and TUI boot.config/: layered config loading + schema + plugin/agent/command discovery.control/: OAuth account/credential management for OpenCode Control (enterprise portal).file/: file abstraction layer — git integration, ignore rules, file watching, ripgrep search, content handling.flag/: feature flags and environment variable registry (30+ flags).global/: singleton paths (XDG-compliant data/config/cache/state directories).id/: monotonic ID generation with ascending/descending timestamp-based IDs.lsp/: language server registry, client lifecycle, diagnostics, symbol/definition operations.mcp/: MCP local/remote clients, OAuth flow, tool/prompt/resource bridge.plugin/: plugin system — codex and copilot plugins for provider-specific behavior.project/: project identity, git/worktree discovery, per-instance scoping.provider/: provider/model inventory, SDK loader, auth/env/config adaptation.pty/: managed pseudo-terminal sessions + websocket attachment.server/: Hono API, SSE event stream, auth, CORS, proxy fallback, route modules.session/: chat/message persistence, prompt loop, stream processor, compaction/summary/retry/status.share/: session sharing — export/import, shareable URLs, SQL persistence.shell/: shell detection (bash/zsh/cmd), platform-specific process tree management.snapshot/: lightweight git snapshot sandbox used for patch/diff/revert tracking.storage/: dual-layer persistence — legacy JSON file storage + SQLite via Drizzle ORM.tool/: core tool definitions and tool registry/gating.util/: shared utilities — context DI, git wrapper, token estimation, async queue, locks, filesystem helpers.worktree/: git worktree management — create/remove/reset isolated sandboxes.
Runtime Entry and Boot Sequence
Section titled “Runtime Entry and Boot Sequence”1) Process entry (src/index.ts)
Section titled “1) Process entry (src/index.ts)”Flow:
- Register unhandled rejection/exception loggers.
- Build yargs CLI and global options (
--print-logs,--log-level). - Middleware initializes log subsystem and sets
AGENT=1/OPENCODE=1env markers. - One-time DB migration gate: if
opencode.dbmarker missing, runJsonMigration.runwith progress bar. - Register commands (
run,serve,attach,agent,mcp,db, etc.). - Parse CLI and route failures through
FormatError+UI.error.
Full command registry: RunCommand, GenerateCommand, AuthCommand, AgentCommand, UpgradeCommand, UninstallCommand, ServeCommand, AttachCommand, TuiThreadCommand, McpCommand, GithubCommand, ExportCommand, ImportCommand, PrCommand, SessionCommand, DbCommand, DebugCommand, WebCommand, StatsCommand, ModelsCommand.
2) Instance bootstrap (src/project/bootstrap.ts)
Section titled “2) Instance bootstrap (src/project/bootstrap.ts)”When a request/session is bound to a project instance:
- Initialize plugin runtime.
- Initialize share subsystem, formatter, LSP, file watcher, file bus, VCS watcher, snapshot cleaner, truncation cleaner.
- Subscribe command execution events (
/initcommand marks project initialized).
All commands use a bootstrap() wrapper that calls Instance.provide() + Instance.dispose() to ensure project/directory context is set up and torn down properly.
Instance and Project Scoping
Section titled “Instance and Project Scoping”Instance context (src/project/instance.ts)
Section titled “Instance context (src/project/instance.ts)”Instance.provide({ directory, fn }) creates a scoped context with:
directory: current working directory for this request/session.worktree: VCS sandbox boundary (git worktree root or/fallback).project: persisted project identity.
Critical behavior:
Instance.state(init, dispose)gives per-directory cached state stores for modules (LSP, MCP, session trackers, etc.).containsPath(filepath)treats paths insidedirectoryorworktreeas in-bound for permission logic.Instance.disposeAll()tears down all cached state when config changes.
Project identification (src/project/project.ts)
Section titled “Project identification (src/project/project.ts)”For git repos:
- Walk up to
.git. - Derive stable project id from root commit list (
git rev-list --max-parents=0 --all). - Resolve top-level + common-dir for worktree semantics. Non-git fallback:
- Project id
global, worktree/.
Result: project identity is stable across nested directories and worktrees.
Bus / Event System
Section titled “Bus / Event System”Architecture overview (src/bus/)
Section titled “Architecture overview (src/bus/)”The bus is a typed event pub/sub system using Zod for runtime schema validation. It’s instance-scoped (per project directory) and provides type-safe event handling.
Core module (src/bus/index.ts)
Section titled “Core module (src/bus/index.ts)”Exports:
Bus.publish(def, properties)— publish typed events to local subscriptions AND GlobalBus.Bus.subscribe(def, callback)— subscribe to specific event type; returns unsubscribe function.Bus.subscribeAll(callback)— wildcard subscription to all events.Bus.once(def, callback)— one-time subscription; callback returns"done"to unsubscribe.Bus.InstanceDisposed— predefined event when instance is disposed.
Internal state per instance: Map<string, Subscription[]> keyed by event type or "*" for wildcard.
Event flow:
- Code calls
Bus.publish(SomeEvent.def, { prop: value }). - Bus creates payload
{ type, properties }. - Calls all subscriptions matching
typeor"*". - Emits to
GlobalBusfor inter-process distribution.
Event type registry (src/bus/bus-event.ts)
Section titled “Event type registry (src/bus/bus-event.ts)”BusEvent.define(type, propertiesSchema)— registers event with Zod schema in global registry.BusEvent.payloads()— returnsz.discriminatedUnion("type", [...all events])for validation.- Global
registry = Map<string, Definition>()tracks all event types.
Inter-process bus (src/bus/global.ts)
Section titled “Inter-process bus (src/bus/global.ts)”GlobalBusis a Node.jsEventEmitterfor cross-process events.- Shape:
{ directory?: string, payload: { type, properties } }. - Allows instances in different processes to communicate; each instance publishes to both local subs and GlobalBus.
Storage Layer
Section titled “Storage Layer”Dual-layer architecture (src/storage/)
Section titled “Dual-layer architecture (src/storage/)”JSON files (legacy) migrating to SQLite. Uses migrations, file-based locks, and lazy-loaded state.
JSON file storage (src/storage/storage.ts)
Section titled “JSON file storage (src/storage/storage.ts)”API:
Storage.read<T>(key: string[])— key arrays map to file paths:["session", "abc123"]→$DATA/storage/session/abc123.json.Storage.write<T>(key, content)— write JSON to computed path.Storage.update<T>(key, fn)— read-modify-write with draft function.Storage.remove(key)— delete file.Storage.list(prefix)— list all keys under prefix.- Uses
Lock.read()andLock.write()for concurrent access control.
Storage hierarchy:
~/.opencode/data/├── storage/│ ├── project/*.json│ ├── session/PROJECT_ID/*.json│ ├── message/SESSION_ID/*.json│ ├── message_diff/SESSION_ID.json│ ├── part/MESSAGE_ID/*.json│ ├── todo/SESSION_ID.json│ ├── permission/PROJECT_ID.json│ ├── session_share/SESSION_ID.json│ └── migration # migration marker file└── opencode.db # SQLite database (new)SQLite database (src/storage/db.ts)
Section titled “SQLite database (src/storage/db.ts)”Uses Drizzle ORM over bun:sqlite.
API:
Database.Client— lazy-initialized Drizzle ORM instance.Database.use(callback)— provides DB client or transaction context.Database.transaction(callback)— wraps in SQLite transaction, runs effects after commit.Database.effect(fn)— queues function to run after transaction commits or immediately if outside transaction.
SQLite pragmas applied:
journal_mode = WAL(Write-Ahead Logging for concurrency).synchronous = NORMAL.busy_timeout = 5000(5s wait for locks).cache_size = -64000(64MB cache).foreign_keys = ON.wal_checkpoint(PASSIVE).
JSON to SQLite migration (src/storage/json-migration.ts)
Section titled “JSON to SQLite migration (src/storage/json-migration.ts)”JsonMigration.run(sqlite, options) migrates all JSON files to SQLite in a single transaction:
- Scans all JSON files via Bun.Glob patterns.
- Projects first (no FK deps) → sessions → messages → parts → todos, permissions, shares.
- Batch processing: 1000 items per batch to avoid OOM.
- Orphan handling: skips records without valid parent IDs, logs warnings.
- Error resilience:
Promise.allSettled()for individual file reads. - Progress callback for TUI progress bar.
Schema (src/storage/schema.ts)
Section titled “Schema (src/storage/schema.ts)”Re-exports all table definitions: ControlAccountTable, SessionTable, MessageTable, PartTable, TodoTable, PermissionTable, SessionShareTable, ProjectTable.
Shared Timestamps columns: time_created (auto-set on insert), time_updated (auto-set on update).
Config System
Section titled “Config System”Architecture (src/config/config.ts)
Section titled “Architecture (src/config/config.ts)”Layered, mergeable config with precedence order (low → high):
- Remote
.well-known/opencode(org defaults). - Global
~/.config/opencode/opencode.json{,c}. - Custom config path (
$OPENCODE_CONFIG). - Project config (
opencode.json{,c}in worktree up to root). .opencode/directories (project & home).- Inline config (
$OPENCODE_CONFIG_CONTENT). - Managed config (
/etc/opencode/opencode.json{,c}— highest priority).
Merging: custom merge() concatenates arrays (plugins, instructions accumulate across layers) instead of replacing.
File interpolation: {env:VAR} and {file:/path} substitution before parsing. JSONC support with comments and trailing commas.
Config.Info schema (key fields)
Section titled “Config.Info schema (key fields)”{ theme?: string keybinds?: Keybinds // 50+ fields with vim-like defaults tui?: { scroll_speed, scroll_acceleration, diff_style } server?: { port, hostname, mdns, mdnsDomain, cors } command?: Record<string, Command> skills?: { paths, urls } plugin?: string[] // npm packages or file:// URLs snapshot?: boolean share?: "manual" | "auto" | "disabled" model?: string // provider/model format small_model?: string default_agent?: string agent?: Record<string, Agent> provider?: Record<string, Provider> mcp?: Record<string, Mcp> formatter?: false | Record<string, FormatterConfig> lsp?: false | Record<string, LSPConfig> instructions?: string[] permission?: Permission // Record<string, "ask"|"allow"|"deny"> compaction?: { auto, prune, reserved } experimental?: { batch_tool, openTelemetry, primary_tools, continue_loop_on_deny, mcp_timeout }}Sub-loaders
Section titled “Sub-loaders”loadCommand(dir)— scans{command,commands}/**/*.md, parses YAML frontmatter viaConfigMarkdown.parse().loadAgent(dir)— scans{agent,agents}/**/*.md.loadMode(dir)— scans{mode,modes}/*.md(legacy, migrated to agent).loadPlugin(dir)— scans{plugin,plugins}/*.{ts,js}, converts tofile://URLs.
Markdown frontmatter parser (src/config/markdown.ts)
Section titled “Markdown frontmatter parser (src/config/markdown.ts)”- Uses
gray-matterfor YAML frontmatter parsing. - Regex patterns:
@filereferences (@path/to/file) and!`command`shell blocks. - Fallback sanitizer for non-YAML-compliant frontmatter: wraps values with colons in block scalars.
Permission schema
Section titled “Permission schema”Permission = Record<string, PermissionAction | PermissionObject>PermissionAction = "ask" | "allow" | "deny"PermissionObject = Record<string, PermissionAction>Fields: read, edit, glob, grep, list, bash, task, external_directory, todowrite, todoread, question, webfetch, websearch, codesearch, lsp, doom_loop, skill, ...File Subsystem
Section titled “File Subsystem”Main module (src/file/index.ts)
Section titled “Main module (src/file/index.ts)”Exports:
File.status()— runsgit diff HEAD --numstat+git ls-files --others+git diff --name-only --diff-filter=D HEAD. ReturnsInfo[]with status:"added" | "modified" | "deleted".File.read(file)— images: fast path via extension check, base64 encode. Binary: empty content. Text: read + generate git diff usingstructuredPatch()fromdiffpackage.File.list(dir)— readdir, exclude.git/.DS_Store, apply gitignore viaignorepackage, sort directories first.File.search(input)— fuzzy search viafuzzysortover pre-cached file list fromRipgrep.files(). Hides hidden files unless query starts with.. Limits: 100 default.
Access control: checks Instance.containsPath() to prevent symlink escapes.
Ignore patterns (src/file/ignore.ts)
Section titled “Ignore patterns (src/file/ignore.ts)”33 folder names to skip: dist, build, out, target, node_modules, .git, .next, __pycache__, .vscode, .idea, .cache, etc.
File globs: **/*.swp, **/*.pyc, **/.DS_Store, **/logs/**, **/coverage/**, etc.
Match algorithm: whitelist check → folder blacklist → file glob match → extra globs.
File watcher (src/file/watcher.ts)
Section titled “File watcher (src/file/watcher.ts)”- Uses
@parcel/watcher(native bindings: inotify on Linux, FSEvents on macOS, Windows filesystem events). - Subscribes to
Instance.directory+.gitseparately. - Filters via
FileIgnore.PATTERNS+ config ignore list. - Publishes
FileWatcher.Event.Updatedvia Bus.
Ripgrep integration (src/file/ripgrep.ts)
Section titled “Ripgrep integration (src/file/ripgrep.ts)”Binary management:
- Checks
Bun.which("rg")first (system ripgrep). - Falls back to
$OPENCODE_BIN/rg. - Auto-downloads v14.1.1 from GitHub if missing — platform-specific (arm64/x64, darwin/linux/win32, musl/glibc variants).
Functions:
Ripgrep.files({ cwd, glob?, hidden, follow, maxDepth })— yields file paths line-by-line viarg --files.Ripgrep.search({ cwd, pattern, glob?, limit, follow })— runsrg --json, parses match events. ReturnsMatch[]with path, line_number, submatches.Ripgrep.tree({ cwd, limit })— builds directory hierarchy via BFS traversal over file list. Returns formatted tree string.
File time locking (src/file/time.ts)
Section titled “File time locking (src/file/time.ts)”Prevents concurrent writes and detects external modifications:
FileTime.read(sessionID, file)— records session read timestamp.FileTime.assert(sessionID, filepath)— checks mtime hasn’t changed since read.FileTime.withLock(filepath, fn)— serializes writes to same file via chained promises.
Session Engine (Core Orchestration)
Section titled “Session Engine (Core Orchestration)”Persistence model (src/session/session.sql.ts)
Section titled “Persistence model (src/session/session.sql.ts)”Tables:
session: metadata, title, parent, summary counters, permission overrides, revert markers.message: user/assistant info JSON.part: message parts (text/tool/reasoning/patch/step/subtask/etc.) JSON.todo: ordered todo rows.permission: persisted project approval rules.
Session CRUD (src/session/index.ts)
Section titled “Session CRUD (src/session/index.ts)”Operations:
Session.create(parentID?, title?)— new session.Session.get(sessionID)— retrieve by ID.Session.fork(sessionID, messageID?)— clone session history up to message.Session.setTitle,setArchived,setPermission,setRevert,setSummary.Session.remove(sessionID)— cascade delete children.Session.list(directory?, roots?, start?, search?, limit?)— paginated query.
Message operations:
updateMessage(msg)— upsert.updatePart(part)— upsert.updatePartDelta(sessionID, messageID, partID, field, delta)— streaming incremental updates.removeMessage,removePart— delete with bus events.
Cost tracking:
Session.getUsage(model, usage, metadata?)— converts token counts to cost viamodel.cost.- Handles Anthropic vs others token accounting (Anthropic includes cached tokens in inputTokens, others exclude).
- Supports over-200k context pricing tier.
- Formula:
(tokens.input * cost.input + tokens.output * cost.output + tokens.cache.read * cost.read + tokens.cache.write * cost.write + tokens.reasoning * cost.output) / 1_000_000.
Bus events: Session.Created, Updated, Deleted, Diff, Error, MessageV2.Event.Updated, Removed, PartUpdated, PartRemoved, PartDelta.
Message schema (src/session/message-v2.ts)
Section titled “Message schema (src/session/message-v2.ts)”Part types (discriminated union):
TextPart— synthetic flag, ignored flag, time tracking, metadata.ReasoningPart— model reasoning content (Reasoning models).FilePart— attached files/images with source tracking (file/symbol/resource).ToolPart— tool invocation with state machine:pending -> running -> completed | error.StepStartPart,StepFinishPart— agentic step boundaries with cost accounting.SnapshotPart,PatchPart— filesystem change tracking.SubtaskPart— delegated subtask with model selection.CompactionPart— message history compaction marker.RetryPart— retry attempts with error info.
Output formats: OutputFormatText (default), OutputFormatJsonSchema (includes schema + retryCount up to 2).
Error types: OutputLengthError, AbortedError, StructuredOutputError, AuthError, APIError, ContextOverflowError.
toModelMessages(...) adaptation:
- Converts internal message/part graph to AI SDK
ModelMessage[]. - Translates tool results into tool-output parts.
- Injects fallback user file parts for providers with weaker media-in-tool-result handling.
- Handles compacted tool outputs with placeholder text.
System prompt construction (src/session/system.ts)
Section titled “System prompt construction (src/session/system.ts)”Model-specific prompt selection:
- gpt-5.2-codex →
PROMPT_CODEX. Gemini →PROMPT_GEMINI. - Claude →
PROMPT_ANTHROPIC. Trinity →PROMPT_TRINITY. Default (Qwen, others) →PROMPT_ANTHROPIC_WITHOUT_TODO.
Environment block injected dynamically: working directory, git status, platform, date — wrapped in <env> tags.
Assembly in LLM.stream(): combines provider() + input.system + user.system. Plugins can transform via experimental.chat.system.transform hook. Maintains 2-part structure for cache efficiency.
Instruction loading (src/session/instruction.ts)
Section titled “Instruction loading (src/session/instruction.ts)”Files scanned (priority order): AGENTS.md, CLAUDE.md, CONTEXT.md (deprecated).
System paths resolution:
- Project-level: walks up from
Instance.directoryto worktree root usingFilesystem.findUp(). Stops at first match. - Global-level: checks
$OPENCODE_CONFIG_DIR/AGENTS.md,~/.opencode/AGENTS.md,~/.claude/CLAUDE.md. First existing file wins. - Config
instructionsarray: supportshttps://URLs (fetched with 5s timeout),~/pathexpansion, absolute paths, relative paths (globUp from project root).
Claiming mechanism: per-messageID map of claimed file paths prevents duplicate inclusion within a single message.
Prompt entry + loop (src/session/prompt.ts)
Section titled “Prompt entry + loop (src/session/prompt.ts)”Primary path:
SessionPrompt.prompt(...)creates user message + parts.- Optionally applies legacy per-prompt
toolspermissions to session permission rules. - Calls
loop({ sessionID })unlessnoReply.
State machine: per-sessionID AbortController + callbacks array. assertNotBusy() throws Session.BusyError if session active.
Loop behavior:
- Pull recent, non-compacted message stream.
- Detect pending
subtaskorcompactionparts and process those first. - Detect context overflow and enqueue auto-compaction.
- Resolve active agent + model.
- Resolve all tools (built-in + MCP) with provider-schema transformation.
- Stream model output via
SessionProcessor.process(...). - If structured JSON output mode is requested, enforce
StructuredOutputtool completion with retry (up to 2 attempts).
LLM stream adapter (src/session/llm.ts)
Section titled “LLM stream adapter (src/session/llm.ts)”LLM.stream builds provider call params:
- System prompt stack (provider default prompt + runtime instructions + user/system additions).
- Provider/agent/model options merge.
- Tool filtering based on permission (
PermissionNext.disabled(...)). - Provider headers and retries.
- Tool call repair hook (invalid casing -> lowercase fallback or
invalidtool).
Streaming event processor (src/session/processor.ts)
Section titled “Streaming event processor (src/session/processor.ts)”Consumes SDK stream events and persists incremental parts:
reasoning-start/delta/end-> reasoning parts.tool-input-startandtool-call/result/error-> tool state lifecycle.text-start/delta/end-> text parts.start-step/finish-step-> snapshot and token/cost accounting.
Important controls:
- Doom loop detection: repeated same tool+input 3 times requests explicit permission.
- Permission/question rejection can halt or continue loop depending on config.
- Retry policy for transient provider errors (
SessionRetry). - Auto-compaction signal when token usage nears model limits.
Compaction and pruning (src/session/compaction.ts)
Section titled “Compaction and pruning (src/session/compaction.ts)”isOverflow(...)compares token usage against model input/context minus reserved buffer.process(...)spawns hiddencompactionagent to summarize prior conversation.prune(...)marks old completed tool outputs as compacted after token budget thresholds, keeping recent context + protected tools.
Session diff summary (src/session/summary.ts)
Section titled “Session diff summary (src/session/summary.ts)”After steps complete:
- Compute aggregate file additions/deletions/files changed via snapshot diff.
- Persist per-session summary stats and per-message diff snippets.
Tooling Subsystem
Section titled “Tooling Subsystem”Tool contract (src/tool/tool.ts)
Section titled “Tool contract (src/tool/tool.ts)”Tool.define(id, init) wraps every tool with:
- Zod parameter validation.
- Unified error on invalid args.
- Output truncation via
Truncate.output(...)unless tool marks itself pre-truncated.
Registry + model-aware gating (src/tool/registry.ts)
Section titled “Registry + model-aware gating (src/tool/registry.ts)”Built-ins + plugins are merged and exposed based on runtime:
- Core tools always loaded (
bash,read,glob,grep,task, etc.). apply_patchpreferred for modern GPT family;edit/writedisabled in that mode.websearch/codesearchgated to opencode provider or explicit env enable.- Optional tools behind flags (
lsp,plan_enter/exit,batch).
Truncation system (src/tool/truncation.ts)
Section titled “Truncation system (src/tool/truncation.ts)”Truncate.output(content)— truncates tool output exceedingMAX_LINES(2000) orMAX_BYTES(50KB).- Saves full output to
Global.Path.data/tool-output/{id}with file path hint in truncation message. - Automatic cleanup after 7 days; scheduler runs hourly.
- Tools can mark themselves as pre-truncated to skip double-truncation.
Tool-by-tool internals
Section titled “Tool-by-tool internals”tool/bash.ts — Shell command execution:
- Lazy-loads tree-sitter bash WASM parser to derive command AST for permission checks.
- Shell detection via
Shell.acceptable()(excludes fish, nu). - Parameters:
command(string),timeout(ms, default 2min),workdir(defaults to Instance.directory),description(5-10 word summary). - Execution: spawns via
child_process.spawn, streams metadata incrementally.MAX_METADATA_LENGTH = 30_000.
tool/read.ts — File/directory reading:
- Parameters:
filePath(absolute),offset(1-indexed line),limit(default 2000 lines). - Constants:
DEFAULT_READ_LIMIT = 2000,MAX_LINE_LENGTH = 2000,MAX_BYTES = 50 * 1024. - Handles: directories (sorted listing), images (base64 encode), PDFs, binary detection, text files.
- Post-read: injects instruction files via
InstructionPrompt.resolve(). - File-not-found: suggests similar filenames from parent directory.
tool/edit.ts — Targeted file editing:
- Robust replacement strategy chain: exact match → trimmed → anchor → whitespace/indent/escape/context-aware.
- Parameters:
filePath,old_string,new_string. - Post-edit: triggers
LSP.touchFile()and reports diagnostics (errors only, max 20 per file). - Uses
FileTime.assert()to detect external modifications before write.
tool/write.ts — Full file write:
- Parameters:
content,filePath. - Post-write: publishes
File.Event.Edited+FileWatcher.Event.Updated, triggers LSP diagnostics. - Constants:
MAX_DIAGNOSTICS_PER_FILE = 20,MAX_PROJECT_DIAGNOSTICS_FILES = 5.
tool/apply_patch.ts — Unified diff/patch application:
- Preferred for modern GPT family models over edit/write.
- Patch format:
*** Begin Patch/*** End Patchwith*** Add/Update/Delete File:headers. - Multi-pass line matching: exact → rstrip → trim → Unicode normalization (smart quotes, em-dashes, ellipsis).
- Returns per-file metadata: additions, deletions, type (add/update/delete/move).
tool/task.ts — Subagent execution:
- Creates child session for subagent execution.
- Enforces anti-recursion depth limits. Subagents cannot use todowrite/todoread.
tool/batch.ts — Parallel tool execution:
- Executes up to 25 tool calls in parallel and emits per-tool parts.
tool/glob.ts — Glob pattern file search:
- Parameters:
pattern(glob string),path?(search directory, defaults to Instance.directory). - Uses
Ripgrep.files()for fast file matching. - Returns max 100 files sorted by mtime (newest first).
- Permission:
globwith pattern allowlisting. - Output: absolute paths, file count, truncation flag.
tool/grep.ts — Content search via ripgrep:
- Parameters:
pattern(regex),path?(search directory),include?(glob filter e.g. “*.js”). - Direct ripgrep invocation with
-nH --hidden --no-messages. - Max 100 matches. Line text capped at 2000 chars.
- Exit code handling: 0=matches found, 1=no matches, 2=errors but may have partial matches.
- Handles broken symlinks silently with
--no-messages.
tool/multiedit.ts — Multi-file editing:
- Parameters:
filePath,edits[]array of{ filePath, oldString, newString, replaceAll? }. - Wraps EditTool, applies edits in sequence to a single file.
- Returns combined result of last edit. Each edit must produce different content.
tool/question.ts — User question:
- Parameters:
questionsarray ofQuestion.Infoobjects with type, options. - Integrates with OpenCode’s Question system — blocks execution until user answers.
- Returns metadata with answers. Session-aware: stores answers in context.
- Gated behind
OPENCODE_ENABLE_QUESTION_TOOLflag.
tool/skill.ts — Skill loading:
- Loads skill payload + sampled files from configured skill paths/URLs.
- Injects a structured skill block into the conversation context.
- Skills are markdown files with YAML frontmatter defining parameters and templates.
tool/plan.ts — Plan/build mode:
plan_enter/plan_exittool pair for mode handoff.- Uses question prompts to confirm mode transitions with user.
- Plan agent denies all edit tools except
.opencode/plans/*.mdfiles. - Gated behind
OPENCODE_EXPERIMENTAL_PLAN_MODEorOPENCODE_EXPERIMENTAL.
tool/webfetch.ts — Web content fetching:
- Parameters:
url(http/https),format?(“text”|“markdown”|“html”, default: markdown),timeout?(seconds, max 120). - Max response 5MB. Image detection: returns base64-encoded images as attachments.
- HTML to markdown conversion via TurndownService.
- Cloudflare bot detection: retry with altered User-Agent.
- Accept header negotiation for requested format.
tool/websearch.ts — Web search:
- Parameters:
query,numResults?(1-100, default 8),livecrawl?,type?(“auto”|“fast”|“deep”),contextMaxCharacters?. - Backend: Exa API (
https://mcp.exa.ai/mcp) via JSON-RPC 2.0 with SSE response. - 25-second timeout. Returns aggregated context text from multiple results.
- Gated to opencode provider or
OPENCODE_ENABLE_EXA.
tool/codesearch.ts — Code search:
- Parameters:
query(natural language),tokensNum(1000-50000, default 5000). - Backend: Exa
get_code_context_exaendpoint. 30-second timeout. - Token count determines context depth. Returns code snippets and documentation.
- Gated to opencode provider.
tool/lsp.ts — LSP tool exposure:
- Operations: goToDefinition, findReferences, hover, documentSymbol, workspaceSymbol, etc.
- Gated behind
OPENCODE_EXPERIMENTAL_LSP_TOOL.
tool/external-directory.ts — External directory permission gating:
- Checks if path is outside project boundary. Requests
external_directorypermission with{ path, kind }metadata.
tool/invalid.ts — Invalid tool handler:
- Catches malformed tool calls and returns helpful error message.
Permission enforcement pattern
Section titled “Permission enforcement pattern”Nearly every mutating or sensitive tool path performs:
- Path boundary check (
assertExternalDirectory). - Permission request via
ctx.ask(...)with patterns + metadata. - Operation execution.
LSP Architecture
Section titled “LSP Architecture”Server registry (src/lsp/server.ts)
Section titled “Server registry (src/lsp/server.ts)”Defines many language server adapters with:
- Root detection strategy (
NearestRoot(...), project markers). - File extension filters.
- Spawn logic (local binaries, npm/bun installs, remote release download fallback).
Client manager (src/lsp/index.ts)
Section titled “Client manager (src/lsp/index.ts)”- Maintains server catalog, connected clients, failed keys, and in-flight spawns.
- Deduplicates client per
(serverID, root). - Exposes operations: hover, definition, references, symbols, call hierarchy, diagnostics.
LSP client protocol bridge (src/lsp/client.ts)
Section titled “LSP client protocol bridge (src/lsp/client.ts)”- JSON-RPC stdio connection via child process spawn.
- Sends initialize capabilities and workspace config.
- Publishes diagnostics via bus with debounce for stabilization (prevents flicker from rapid diagnostic updates).
notify.open(...)usesdidOpen/didChangeand watched-file notifications.- Per-client state: capabilities, open documents, pending requests, diagnostic buffer.
Language detection (src/lsp/language.ts)
Section titled “Language detection (src/lsp/language.ts)”Maps file extensions to language server IDs for automatic server selection. Covers TypeScript, Python, Rust, Go, Java, C/C++, Ruby, PHP, and many more.
MCP Architecture
Section titled “MCP Architecture”Core (src/mcp/index.ts)
Section titled “Core (src/mcp/index.ts)”Supports:
- Local stdio MCP servers.
- Remote StreamableHTTP and SSE transports.
- OAuth-enabled remote auth with callback server and persisted token/state handling.
Tool conversion path:
client.listTools()-> sanitize schema ->dynamicTool(...)wrappers.- Tools are namespaced as
<server>_<tool>. - Per-server/request timeouts configurable.
Also exposes prompt/resource discovery and direct prompt/resource reads.
OAuth flow (src/mcp/oauth-callback.ts, src/mcp/oauth-provider.ts, src/mcp/auth.ts)
Section titled “OAuth flow (src/mcp/oauth-callback.ts, src/mcp/oauth-provider.ts, src/mcp/auth.ts)”oauth-callback.ts: spins up local HTTP server for OAuth redirect handling. Serves callback page that captures auth code.oauth-provider.ts: implements MCP OAuth provider interface with persisted token/state. Handles PKCE code challenge, token exchange, and refresh.auth.ts: credential storage and retrieval for MCP servers. Persists tokens per-server in config directory.
PTY + Server Layer
Section titled “PTY + Server Layer”PTY manager (src/pty/index.ts)
Section titled “PTY manager (src/pty/index.ts)”- Spawns terminal sessions via
bun-ptynative bindings. - Keeps rolling output buffer (2MB cap) and cursor metadata per PTY session.
- Supports reconnect with cursor-based replay — clients send their last cursor position, server replays buffered output from that point.
- Each PTY has: id, shell path, working directory, environment, dimensions (rows/cols).
- Lifecycle: create → connect (WebSocket) → write input → read output → resize → destroy.
HTTP server (src/server/server.ts)
Section titled “HTTP server (src/server/server.ts)”- Hono app with OpenAPI descriptions on every route.
- Basic auth support via
OPENCODE_SERVER_PASSWORD/OPENCODE_SERVER_USERNAMEenv flags. - CORS allowlist + localhost/tauri automatic handling.
- Instance binding middleware: every request runs inside
Instance.provide(...)scope, ensuring project context is set. - SSE
/eventstream for all bus events + 10-second heartbeat to keep proxies alive. - All route modules lazily loaded via
lazy()to minimize startup cost. - Route modules: project, session, pty, mcp, file, config, provider, question, permission, tui, experimental, global.
Server error handling (src/server/error.ts)
Section titled “Server error handling (src/server/error.ts)”Standard error responses:
- 400:
{ data, errors[], success: false }. - 404: NotFoundError schema.
Server events (src/server/event.ts)
Section titled “Server events (src/server/event.ts)”Event.Connected—server.connectedbus event (sent on SSE connection).Event.Disposed—global.disposedbus event (sent on shutdown).
Agent System
Section titled “Agent System”Agent definitions (src/agent/agent.ts)
Section titled “Agent definitions (src/agent/agent.ts)”Agent.Info schema: { name, description?, mode: "subagent"|"primary"|"all", native?, hidden?, topP?, temperature?, color?, permission: PermissionNext.Ruleset, model?, variant?, prompt?, options, steps? }.
Built-in agents
Section titled “Built-in agents”- build (primary, visible) — default agent. Full tool permissions. Allows questions and plan mode.
- plan (primary, visible) — planning mode. Denies all edit tools except
.opencode/plans/*.md. Forcesplan_exit. - general (subagent) — multi-task executor. Denies todo read/write.
- explore (subagent) — read-only. Allows: grep, glob, list, bash, webfetch, websearch, codesearch, read.
- compaction (primary, hidden) — session summarization. Minimal permissions.
- title (primary, hidden) — auto-generate session title. Temperature 0.5.
- summary (primary, hidden) — session summarization.
Agent loading
Section titled “Agent loading”Lazy-loaded via Instance.state(): builds permission defaults, merges user config from cfg.agent overrides, caches per instance.
Agent generation
Section titled “Agent generation”Agent.generate({ description, model? }) uses generateObject() with Zod schema to create new agents: { identifier, whenToUse, systemPrompt }.
Snapshot System
Section titled “Snapshot System”Git snapshot sandbox (src/snapshot/)
Section titled “Git snapshot sandbox (src/snapshot/)”Location: ~/.opencode/data/snapshot/{projectID}/ — a separate .git directory per project (not the project’s own git repo).
Operations:
Snapshot.track()— initializes git repo if needed, stages all files, returns tree hash viagit write-tree. Disablescore.autocrlfon Windows.Snapshot.patch(hash)— stages current files, diffs against hash viagit diff --name-only. Returns{ hash, files: string[] }.Snapshot.restore(hash)—git read-tree+git checkout-index -a -fto restore filesystem state.Snapshot.revert(patches)— iterates patches,git checkout {hash} -- {file}per file. Deletes files that didn’t exist in snapshot.Snapshot.diff(hash)— full unified diff text against current state.Snapshot.diffFull(from, to)— per-file diffs with before/after content, additions/deletions counts, status (added/deleted/modified). Usesgit showto retrieve content at each tree.Snapshot.cleanup(prune)— runsgit gc --prune={date}on snapshot repo. Hourly scheduler.
Skips operations when config.snapshot === false or Flag.OPENCODE_CLIENT === "acp".
ACP (Agent Client Protocol)
Section titled “ACP (Agent Client Protocol)”Agent bridge (src/acp/agent.ts)
Section titled “Agent bridge (src/acp/agent.ts)”ACP.Agent implements external ACPAgent interface, bridging OpenCode sessions/tools/permissions to external clients.
Capabilities: newSession(), loadSession(), unstable_listSessions(), unstable_forkSession(), unstable_resumeSession(), unstable_setSessionModel(), setSessionMode(), prompt(), cancel(), initialize().
Event subscription loop: subscribes to GlobalBus, forwards events:
permission.asked→ forwards permission requests (maps to ACPToolKind).message.part.updated→ tracks tool execution state (pending→running→completed→error).message.part.delta→ streams text/reasoning chunks.
Session manager (src/acp/session.ts)
Section titled “Session manager (src/acp/session.ts)”In-memory mapping of ACP session IDs to OpenCode state: { id, cwd, mcpServers, createdAt, model?, variant?, modeId? }.
Plugin System
Section titled “Plugin System”Plugin architecture (src/plugin/index.ts)
Section titled “Plugin architecture (src/plugin/index.ts)”Builtin plugins: ["opencode-anthropic-auth@0.0.13"]. Internal plugins: CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin.
Hook system: auth, event, tool, tool.definition, chat.headers, config. Triggered via Plugin.trigger(name, input, output).
Codex plugin (src/plugin/codex.ts)
Section titled “Codex plugin (src/plugin/codex.ts)”OpenAI Codex integration. Auth: browser OAuth (PKCE, auth.openai.com) or headless device flow or manual API key. Model filtering: gpt-5.1-codex-max, gpt-5.1-codex-mini, gpt-5.2, gpt-5.2-codex. Token management: auto-refresh, JWT accountId extraction, ChatGPT-Account-Id header. Endpoint: chatgpt.com/backend-api/codex/responses.
Copilot plugin (src/plugin/copilot.ts)
Section titled “Copilot plugin (src/plugin/copilot.ts)”GitHub Copilot integration. Device code auth flow. Custom headers: x-initiator, Copilot-Vision-Request, Openai-Intent. Zero-cost model (included with subscription).
Share System
Section titled “Share System”Share module (src/share/share-next.ts)
Section titled “Share module (src/share/share-next.ts)”Service endpoint: configurable (default: https://opncd.ai).
API calls:
POST /api/share→ create share (returns id, url, secret).POST /api/share/{id}/sync→ sync data batches.DELETE /api/share/{id}→ remove share.
Synced data types (discriminated union):
{ type: "session", data: SDK.Session }— session metadata.{ type: "message", data: SDK.Message }— message envelope.{ type: "part", data: SDK.Part }— message part (text/tool/reasoning/etc).{ type: "session_diff", data: SDK.FileDiff[] }— file change diffs.{ type: "model", data: SDK.Model[] }— models used in session.
Sync model:
- Event subscriptions:
Session.Event.Updated→ sync session,MessageV2.Event.Updated→ sync message + models,MessageV2.Event.PartUpdated→ sync part,Session.Event.Diff→ sync diffs. - Debounced batching (1-second delay): queues updates by sessionID, batches multiple updates, uses ULID for deduplication.
- Full sync on share creation: syncs entire session history (metadata, all messages, all parts, file diffs, models).
Disable: OPENCODE_DISABLE_SHARE=true or config.share === "disabled".
SQL schema (src/share/share.sql.ts)
Section titled “SQL schema (src/share/share.sql.ts)”SessionShareTable: session_id (PK, FK to Session), id (share ID), secret (auth secret), url (share URL), timestamps.
Provider Layer (Deep)
Section titled “Provider Layer (Deep)”Provider state building (src/provider/provider.ts, ~1328 lines)
Section titled “Provider state building (src/provider/provider.ts, ~1328 lines)”Provider.state() builds the complete inventory via lazy state machine:
- Database bootstrap — loads
ModelsDev.get()from cache orhttps://models.dev/api.json. Transforms viafromModelsDevProvider(). - GitHub Copilot Enterprise injection — creates synthetic provider by duplicating and remapping.
- Config merging — extends with
config.provider.*overrides. Per-model: cost, capabilities, options, headers, variants. - Environment variable detection — scans providers for matching env vars. Sets
source: "env". - Auth credential loading — loads OAuth tokens + API keys from Auth storage. Plugin-based loader hooks.
- Custom loader execution —
CUSTOM_LOADERS[providerID](dbProvider)for provider-specific logic. - Config finalization — applies blacklist/whitelist, filters deprecated/alpha models, deletes empty providers.
Provider-specific adapters (CUSTOM_LOADERS)
Section titled “Provider-specific adapters (CUSTOM_LOADERS)”Anthropic:
- Custom header:
anthropic-beta: claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14. - Opts into extended thinking and tool streaming features. autoload: false.
OpenCode:
- Filters paid models without auth (cost.input > 0). Free tier gets
apiKey: "public". - autoload: true if any models remain after filtering.
OpenAI:
- Uses
sdk.responses(modelID)for Responses API (structured output). autoload: false.
GitHub Copilot / Enterprise:
- Version detection:
shouldUseCopilotResponsesApi()for GPT-5+ models. - GPT-5+ →
sdk.responses(modelID), others →sdk.chat(modelID). - Dual auth handling for standard vs enterprise accounts. autoload: false.
Amazon Bedrock (most complex adapter):
- Cross-region inference profiles: detects global., us., eu., jp., apac., au. prefixes.
- Region resolution: config →
AWS_REGION→us-east-1. - US region: adds “us.” prefix for nova/claude/deepseek. Skips in GovCloud regions.
- EU region: “eu.” prefix for specific regions + claude/nova/llama3/pixtral.
- APAC: jp. prefix for Tokyo, au. for Australia (Claude), apac. for others.
- Credential chain: Bearer token (
AWS_BEARER_TOKEN_BEDROCK) → auth.key → profile-based → IAM/WebIdentity → container credentials via@aws-sdk/credential-providers. - autoload: checks region + at least one auth method present.
Google Vertex AI:
- Dynamic auth via
google-auth-library(dynamically installed viaBunProc.install()). - Custom fetch wrapper injects Bearer token from application default credentials.
- Project from
GOOGLE_CLOUD_PROJECT/GCP_PROJECT. Location fromGOOGLE_CLOUD_LOCATION/VERTEX_LOCATIONorus-central1. - autoload: true if project is set.
GitLab:
- Instance URL from
GITLAB_INSTANCE_URLorhttps://gitlab.com. - API key from OAuth access > API key >
GITLAB_TOKENenv. - Feature flags:
duo_agent_platform_agentic_chat. Custom User-Agent with opencode version. - Model method:
sdk.agenticChat(modelID, aiGatewayHeaders, featureFlags).
Cloudflare Workers AI / AI Gateway:
- Workers AI: requires
CLOUDFLARE_ACCOUNT_ID+CLOUDFLARE_API_KEY. BaseURL constructed from account ID. - AI Gateway: requires account ID +
CLOUDFLARE_GATEWAY_ID+ API token. Usesai-gateway-providerv2.x. Model IDs inprovider/modelformat.
OpenRouter, Vercel: Custom headers (HTTP-Referer: https://opencode.ai/, X-Title: opencode). autoload: false.
SAP AI Core: Requires AICORE_SERVICE_KEY. Options: deploymentId, resourceGroup.
Cerebras: Header X-Cerebras-3rd-Party-Integration: opencode.
Models.dev data (src/provider/models.ts)
Section titled “Models.dev data (src/provider/models.ts)”- Hourly background refresh with 10s timeout.
- Fallback chain:
OPENCODE_MODELS_PATH→ cached file (~/.opencode/cache/models.json) → bundled snapshot → fetch fromOPENCODE_MODELS_URL. - Schema per model: capabilities (temperature, reasoning, attachment, tool_call), modalities (text/audio/image/video/pdf input/output), pricing (input/output/cache/over-200k tiers), limits (context/input/output), status (alpha/beta/deprecated/active).
- Uses
Installation.USER_AGENTheader. Silent failures — uses stale cache on network error.
SDK resolution and dynamic install
Section titled “SDK resolution and dynamic install”- Options assembly: merge provider + model options + headers.
- BaseURL resolution:
${VAR}→ env substitution. - Custom fetch wrapper: timeout via
AbortSignal.timeout(). OpenAI-specific: stripsitemIdmetadata. - SDK cache key: xxHash32 of
{ providerID, npm, options }. - Bundled providers: amazon-bedrock, anthropic, azure, google, vertex, openai, openai-compatible, xai, mistral, groq, deepinfra, cerebras, cohere, gateway, togetherai, perplexity, vercel, gitlab.
- Dynamic:
BunProc.install(model.api.npm, "latest"), resolves firstcreateXxxexport.
Model lookup and selection
Section titled “Model lookup and selection”getModel(providerID, modelID)— fuzzy matching viafuzzysort(threshold -10000).getSmallModel()— priority: claude-4-haiku-5, haiku-4-5, gemini-3-flash, gpt-5-nano. Provider-specific adjustments.defaultModel()— readsconfig.modelor recent history from~/.opencode/state/model.json.sort()— priority keywords: gpt-5.2-codex, claude-4-6-sonnet, big-pickle, gemini-3.1-pro-preview. Then-latest. Then alphabetical desc.parseModel("provider/model/id")— splits on/, modelID can contain slashes.
Provider.Model type
Section titled “Provider.Model type”{ id, providerID, api: { id, url, npm }, name, family?, capabilities: { temperature, reasoning, attachment, toolcall, input/output: { text, audio, image, video, pdf }, interleaved }, cost: { input, output, cache: {read, write}, experimentalOver200K? }, limit: { context, input?, output }, status: "alpha"|"beta"|"deprecated"|"active", options, headers, release_date, variants }Session Support Modules (Deep Pass)
Section titled “Session Support Modules (Deep Pass)”Revert lifecycle (src/session/revert.ts)
Section titled “Revert lifecycle (src/session/revert.ts)”SessionRevert.revert(...)is not a simple pointer rollback; it reconstructs a safe revert boundary by walking all messages/parts and tracking the latest user turn.- Revert target selection rule:
- If caller gives
messageIDonly, revert from that message. - If caller gives
partID, it may still escalate to full-message revert if no meaningful parts (text/tool) remain before that part. - If assistant-side partial revert would leave invalid turn structure, boundary is moved back to the last user message.
- If caller gives
- Patch rollback rule:
- While scanning, all
patchparts after the revert boundary are collected. Snapshot.revert(patches)applies inverse patch set to filesystem state.- Revert snapshot is persisted (
session.revert.snapshot) so unrevert can restore the pre-revert workspace.
- While scanning, all
- Diff/summary recomputation is immediate:
- Recompute message range from revert point.
SessionSummary.computeDiff(...)recalculates file-level deltas.- Persist to
Storage["session_diff", sessionID]and publishSession.Event.Diff. - Update session summary counters (
additions,deletions,files) insideSession.setRevert(...).
unrevert(...)restores snapshot and clears revert marker.cleanup(...)finalizes revert by deleting post-boundary DB rows:- Deletes full
messagerows when reverting whole turns. - Deletes trailing
partrows when reverting within a message. - Publishes
MessageV2.Event.RemovedandMessageV2.Event.PartRemovedfor UI/state convergence.
- Deletes full
Retry policy (src/session/retry.ts, used by src/session/processor.ts)
Section titled “Retry policy (src/session/retry.ts, used by src/session/processor.ts)”SessionRetry.retryable(...)classifies provider/runtime errors into retryable vs terminal.- Hard stop class:
ContextOverflowErroris never retried (must compact/prune instead). - APIError branch:
- Honors provider
isRetryableflag. - Parses known provider body fragments (overload, free-tier exhaustion).
- Produces user-facing retry status message.
- Honors provider
- Delay strategy (
SessionRetry.delay(...)):- Highest priority:
retry-after-msheader. - Next:
retry-afterseconds/date parsing. - Fallback: exponential backoff (
2s * 2^(attempt-1)) with 30s cap when no headers.
- Highest priority:
sleep(ms, signal)is abort-aware and clamps to max JS timeout int; retry waits terminate immediately on session cancel.- In
SessionProcessor.process(...), retry path updatesSessionStatusto{type:"retry", attempt, message, next}before waiting, then re-enters stream loop.
Session activity state (src/session/status.ts)
Section titled “Session activity state (src/session/status.ts)”- State model is a union:
idle | busy | retry. - Storage is per-instance in-memory via
Instance.state(...), keyed bysessionID. set(...)always publishessession.status; legacysession.idleis also emitted for backwards compatibility when transitioning to idle.- Design intent: low-latency ephemeral status bus for UI progress indicators, not durable database state.
Todo persistence (src/session/todo.ts)
Section titled “Todo persistence (src/session/todo.ts)”- Todo rows are session-scoped and ordered (
positioncolumn inTodoTable). - Update semantics are full-replace, transactional:
- Delete all existing todos for session.
- Insert new ordered list.
- Publish
todo.updatedevent with full list payload.
- Read path (
get) returns stable order byposition. - Consequence: this avoids partial merge complexity and keeps tool protocol simple (
todowritealways submits complete authoritative list).
OpenOxide Blueprint (Rust Monolith Mapping)
Section titled “OpenOxide Blueprint (Rust Monolith Mapping)”Recommended Rust module mapping (no client-server split):
crate::instance: context-scoped state cache (equivalent ofInstance.state).crate::session: prompt loop + stream processor + storage models.crate::tool: trait-based tool registry with runtime gating.crate::provider: model/provider adapters with per-provider option transformers.crate::lsp: server registry + client pool keyed by(language_server, root).crate::mcp: async transport manager + tool bridge.crate::snapshot: git-tree snapshot + diff/revert utility.crate::pty: terminal sessions + replay buffers.crate::bus: typed event pub/sub with tokio broadcast channels.crate::config: layered config loading with serde + schemars.crate::file: ripgrep integration, ignore patterns, file watching.crate::agent: agent definitions, permission rulesets, built-in agents.
Rust crate suggestions:
- Async/runtime:
tokio,futures,tokio-util. - Persistence:
sqlx(SQLite) orrusqlite+ typed wrappers. - Serialization/schema:
serde,schemars. - LSP/JSON-RPC:
tower-lsp(or custom jsonrpc over stdio). - Git ops:
gixor subprocess strategy for parity-first implementation. - Glob/grep/search:
ignore,globset,ripgrepsubprocess for fidelity. - HTTP server:
axum+tower(equivalent of Hono). - Event bus:
tokio::sync::broadcastor custom typed channel system. - File watching:
notifycrate (cross-platform inotify/FSEvents/ReadDirectoryChanges). - Terminal:
ratatui+crossterm.
Server Routes (Full HTTP API)
Section titled “Server Routes (Full HTTP API)”Session routes (/api/session)
Section titled “Session routes (/api/session)”GET /— list sessions (directory, search, limit filters).GET /status— all session statuses.GET /:id— get session.GET /:id/children— forked children.POST /— create.DELETE /:id— delete.PATCH /:id— update (title, archive).POST /:id/init— initialize with AGENTS.md.POST /:id/fork— fork at message point.POST /:id/abort— stop processing.POST /:id/share/DELETE /:id/share— shareable link.GET /:id/diff— file changes.POST /:id/summarize— compact via AI.GET /:id/message— all messages.POST /:id/message— send message (streams response).POST /:id/prompt_async— async message (fire-and-forget, 204).POST /:id/command— execute command.POST /:id/shell— run shell command.POST /:id/revert/POST /:id/unrevert— undo/redo.
Project routes (/api/project)
Section titled “Project routes (/api/project)”GET /— list all.GET /current— active project.PATCH /:id— update (name, icon, commands).
File routes (/api/file)
Section titled “File routes (/api/file)”GET /find?pattern=...— ripgrep search (10-item default limit).GET /find/file?query=...— fuzzy file search.GET /find/symbol?query=...— LSP symbol search (stub).GET /file?path=...— list directory.GET /file/content?path=...— read file.GET /file/status— git status.
PTY routes (/api/pty)
Section titled “PTY routes (/api/pty)”- CRUD for PTY sessions + WebSocket upgrade for real-time I/O.
- WebSocket protocol:
onOpen→Pty.connect(),onMessage→ forward input,onClose→ cleanup.
MCP routes (/api/mcp)
Section titled “MCP routes (/api/mcp)”GET /— server status.POST /— add server.POST /:name/auth— start OAuth.POST /:name/auth/callback— complete OAuth.POST /:name/connect/disconnect— manage connections.DELETE /:name/auth— remove OAuth credentials.
Config routes (/api/config)
Section titled “Config routes (/api/config)”GET /— project config.PATCH /— update.GET /providers— list providers with defaults.
Provider routes (/api/provider)
Section titled “Provider routes (/api/provider)”GET /— all providers (connected + defaults).GET /auth— available auth methods.POST /:id/oauth/authorize/callback— OAuth flow.
Question routes (/api/question)
Section titled “Question routes (/api/question)”GET /— pending questions.POST /:id/reply— answer.POST /:id/reject— reject.
Permission routes (/api/permission)
Section titled “Permission routes (/api/permission)”GET /— pending permissions.POST /:id/reply— grant/deny ("once" | "always" | "reject").
TUI routes (/api/tui)
Section titled “TUI routes (/api/tui)”POST /append-prompt,/open-help,/open-sessions,/open-themes,/open-models,/submit-prompt,/clear-prompt,/execute-command,/show-toast,/publish,/select-session./control/next— get next TUI request (AsyncQueue consumer)./control/response— submit response.
Experimental routes (/api/experimental)
Section titled “Experimental routes (/api/experimental)”GET /tool/ids— list tool IDs.GET /tool— tools with schemas for provider/model.- Worktree CRUD:
POST,GET,DELETE /worktree.POST /worktree/reset.
Global routes (/api/global)
Section titled “Global routes (/api/global)”GET /health— version.GET /event— SSE stream (10s heartbeat).GET /config/PATCH /config— global config.POST /dispose— shutdown all instances.
mDNS discovery (src/server/mdns.ts)
Section titled “mDNS discovery (src/server/mdns.ts)”Uses bonjour-service to advertise OpenCode server on local network.
Worktree Management (src/worktree/index.ts)
Section titled “Worktree Management (src/worktree/index.ts)”Git worktree operations for isolated sandboxes (agent execution environments):
Create
Section titled “Create”Worktree.create({ name?, startCommand? }):
- Generates
{adjective}-{noun}names (e.g., “brave-cabin”) to avoid conflicts. - Runs
git worktree add -b {branch} {path} HEAD. - Startup sequence: runs project’s configured start command → runs optional worktree-specific command.
- Emits
Worktree.Event.ReadyorWorktree.Event.Failedvia GlobalBus. - Queues start scripts with a timeout (10s) for the initial setup.
Remove
Section titled “Remove”Worktree.remove({ directory }):
- Locates worktree via
git worktree list --porcelain. - Removes with
git worktree remove --force. - Falls back to manual
fs.rm(target, { recursive: true, force: true, maxRetries: 5 }). - Deletes the associated branch via
git branch -D. - Handles case where worktree directory exists but git doesn’t track it.
Worktree.reset({ directory }):
- Cannot reset the primary workspace (safety check).
- Remote detection: origin → upstream → single remote.
- Default branch detection: remote HEAD → refs/heads/main → refs/heads/master.
- Fetch from remote, then
git reset --hard {target}. - Full cleanup:
git clean -fdx,git submodule update --init --recursive --force,git submodule foreach --recursive git reset --hard,git submodule foreach --recursive git clean -fdx. - Verifies clean state via
git status --porcelain=v1. - Re-runs project start scripts after reset.
Utility Modules
Section titled “Utility Modules”Context DI (src/util/context.ts)
Section titled “Context DI (src/util/context.ts)”Context.create(name) → { use(), provide(value, fn) } via AsyncLocalStorage. Used by Database (transactions), Instance scoping.
Git wrapper (src/util/git.ts)
Section titled “Git wrapper (src/util/git.ts)”git(args, { cwd, env? }) — when OPENCODE_CLIENT === "acp", uses Bun.spawn() with stdin: "ignore" to avoid pipe deadlock.
Token estimation (src/util/token.ts)
Section titled “Token estimation (src/util/token.ts)”Token.estimate(input) — Math.round(input.length / 4). Simple character-based heuristic.
AsyncQueue (src/util/queue.ts)
Section titled “AsyncQueue (src/util/queue.ts)”Consumer-producer pattern: push(item), next(), async iterable. Also work(concurrency, items, fn) for parallel batch processing.
Reader-writer lock (src/util/lock.ts)
Section titled “Reader-writer lock (src/util/lock.ts)”Lock.read(key) / Lock.write(key) — multiple readers, exclusive writer, writer priority. Returns disposable.
RPC (src/util/rpc.ts)
Section titled “RPC (src/util/rpc.ts)”Worker-to-main communication: { type: "rpc.request", method, input, id } / { type: "rpc.result", result, id }.
Filesystem (src/util/filesystem.ts)
Section titled “Filesystem (src/util/filesystem.ts)”exists(), isDir(), normalizePath(), overlaps(a, b), contains(parent, child), findUp(), up(), globUp().
Other utils
Section titled “Other utils”util/wildcard.ts— shell-style pattern matching, glob → regex conversion.util/lazy.ts—lazy(fn)deferred init withreset().util/signal.ts—signal()returns{ trigger(), wait() }for synchronization.util/log.ts— structured logging.util/format.ts— formatting helpers.util/color.ts— terminal colors.util/archive.ts— tar.gz/zip extraction.util/keybind.ts— keybind parsing.util/abort.ts— abort signal utilities.
Bun Runtime Integration
Section titled “Bun Runtime Integration”Package registry (src/bun/registry.ts)
Section titled “Package registry (src/bun/registry.ts)”PackageRegistry.info(pkg, field, cwd?)— callsbun info pkg field, returns string value.PackageRegistry.isOutdated(pkg, cachedVersion, cwd?):- Fetches latest version from registry.
- If cachedVersion is range (
^,~,*,x,<,>,|,=): usessemver.satisfies(). - If pinned: uses
semver.order()to detect downgrade. - Returns boolean.
Process management (src/bun/index.ts)
Section titled “Process management (src/bun/index.ts)”BunProc.run(cmd[], options?)— spawns bun process with stdout/stderr capture. SetsBUN_BE_BUN=1. Returns exit code or throws on non-zero.BunProc.install(pkg, version="latest"):- Write-locked to prevent concurrent installs of the same package.
- Cache location:
~/.opencode/cache/node_modules/{pkg}. - Version strategy: if already installed exact version → return cached path. If latest → check
isOutdated(), skip if current. - Install via:
bun add --force --exact [--no-cache] --cwd ~/.opencode/cache pkg@version. - On success: reads installed
package.jsonto resolve actual version. Updates~/.opencode/cache/package.jsonwith dependency entry. - Returns absolute path to installed module. Throws
InstallFailedErroron failure. - Registry resolution: Bun handles
.npmrcautomatically, no--registryflag needed.
Control System (src/control/)
Section titled “Control System (src/control/)”OAuth credentials for OpenCode Control (enterprise portal):
Account management (src/control/index.ts)
Section titled “Account management (src/control/index.ts)”Control.account()— queries DB for active account (active=true). Synchronous DB read. Returns{ email, url }or undefined.Control.token():- Get active account from DB.
- Check if
token_expiry > Date.now()— if yes, returnaccess_token(valid). - If expired: refresh via
POST {url}/oauth/tokenwithrefresh_tokengrant. - Update DB with new
access_token,refresh_token,token_expiry. - Return new token or undefined if refresh fails.
SQL schema (src/control/control.sql.ts)
Section titled “SQL schema (src/control/control.sql.ts)”ControlAccountTable:
- Columns:
email(text),url(text),access_token(text),refresh_token(text),token_expiry(integer, optional, Unix ms),active(integer, boolean, default false), timestamps. - Primary key:
(email, url)— composite allows multiple Control instances per user. activeflag selects which account to use (one at a time).
Feature Flags (src/flag/flag.ts)
Section titled “Feature Flags (src/flag/flag.ts)”30+ environment variable flags, all read at module load time except dynamic getters:
Core flags
Section titled “Core flags”OPENCODE_AUTO_SHARE— auto-share sessions.OPENCODE_GIT_BASH_PATH— custom Git Bash path (Windows).OPENCODE_CONFIG— custom config file path.OPENCODE_CONFIG_CONTENT— inline config JSON.OPENCODE_DISABLE_AUTOUPDATE— skip auto-update checks.OPENCODE_DISABLE_PRUNE— disable message pruning.OPENCODE_DISABLE_TERMINAL_TITLE— don’t set terminal title.OPENCODE_PERMISSION— global permission override.OPENCODE_DISABLE_DEFAULT_PLUGINS— skip builtin plugins.OPENCODE_DISABLE_LSP_DOWNLOAD— don’t download language servers.OPENCODE_ENABLE_EXPERIMENTAL_MODELS— show alpha/beta models.OPENCODE_DISABLE_AUTOCOMPACT— disable automatic compaction.OPENCODE_DISABLE_MODELS_FETCH— don’t refresh models.dev data.OPENCODE_SERVER_PASSWORD/OPENCODE_SERVER_USERNAME— basic auth credentials.OPENCODE_ENABLE_QUESTION_TOOL— enable question tool.OPENCODE_DISABLE_FILETIME_CHECK— skip file modification time checks.
Claude Code compatibility flags
Section titled “Claude Code compatibility flags”OPENCODE_DISABLE_CLAUDE_CODE— master toggle, disables prompt and skills.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT— don’t use Claude Code system prompt.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS— don’t load Claude Code skills.OPENCODE_DISABLE_EXTERNAL_SKILLS— don’t load any external skills.
Experimental flags
Section titled “Experimental flags”OPENCODE_EXPERIMENTAL— master toggle for all experimental features.OPENCODE_EXPERIMENTAL_FILEWATCHER/OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER— file watcher control.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY— project icon auto-detection.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS— custom bash timeout.OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX— custom output token limit.OPENCODE_EXPERIMENTAL_OXFMT— experimental formatter.OPENCODE_EXPERIMENTAL_LSP_TY— ty language server.OPENCODE_EXPERIMENTAL_LSP_TOOL— LSP as a tool.OPENCODE_EXPERIMENTAL_PLAN_MODE— plan/build agent mode.OPENCODE_EXPERIMENTAL_MARKDOWN— markdown rendering.OPENCODE_ENABLE_EXA— enable Exa web/code search.
Dynamic getters (via Object.defineProperty)
Section titled “Dynamic getters (via Object.defineProperty)”OPENCODE_CLIENT— evaluated at access time, default"cli". Set to"acp"for Agent Client Protocol mode.OPENCODE_DISABLE_PROJECT_CONFIG— evaluated at access time for runtime tooling overrides.OPENCODE_CONFIG_DIR— evaluated at access time for runtime config directory changes.
Global Paths (src/global/index.ts)
Section titled “Global Paths (src/global/index.ts)”XDG-compliant directory layout:
Global.Path.home — os.homedir() or OPENCODE_TEST_HOMEGlobal.Path.data — XDG_DATA_HOME/opencodeGlobal.Path.bin — data/bin (ripgrep, language servers)Global.Path.log — data/logGlobal.Path.cache — XDG_CACHE_HOME/opencode (models.json, node_modules)Global.Path.config — XDG_CONFIG_HOME/opencode (opencode.json)Global.Path.state — XDG_STATE_HOME/opencode (model.json recent history)Cache versioning: auto-clears cache if CACHE_VERSION changes (currently v21). Prevents stale cached data after updates.
ID Generation (src/id/id.ts)
Section titled “ID Generation (src/id/id.ts)”Identifier.ascending(prefix) / descending(prefix) — monotonic timestamp-based IDs.
Format: {PREFIX}_{TIMESTAMP_HEX}{RANDOM}.
Prefixes: session, message, permission, question, user, part, pty, tool.
Monotonic guarantees: maintains last timestamp + counter. If called within same millisecond, increments counter. Ensures IDs are strictly ordered even under high throughput. Descending variant inverts timestamp for reverse-chronological ordering.
Shell Detection (src/shell/shell.ts)
Section titled “Shell Detection (src/shell/shell.ts)”Shell.preferred()—process.env.SHELLor platform fallback.Shell.acceptable()— non-blacklisted shell. Blacklist: fish, nu (incompatible with interactive I/O).Shell.killTree(proc)— cross-platform process tree kill.
Platform support:
- Windows: CMD, Git Bash, or Git-provided
bash.exe(viaOPENCODE_GIT_BASH_PATH). - macOS:
/bin/zsh. - Linux: bash or
/bin/sh.
Architectural Patterns (Cross-Cutting)
Section titled “Architectural Patterns (Cross-Cutting)”Lazy initialization
Section titled “Lazy initialization”All routes, state, and heavy modules use lazy(fn) pattern — deferred computation on first access with optional reset(). Every server route file exports a lazily-constructed Hono router. State objects (Provider.state(), Config.state(), Agent.state()) are all lazy-loaded per instance.
Instance-scoped state
Section titled “Instance-scoped state”Instance.state(init, dispose) provides per-directory cached state. Used by Agent, Config, InstructionPrompt, Bus, File, LSP, MCP, and more. Disposed on Instance.disposeAll() (triggered by config changes). This is the primary mechanism for multi-tenant isolation — each project directory gets its own independent state.
Event-driven architecture
Section titled “Event-driven architecture”Server publishes events via GlobalBus; clients listen via SSE /api/global/event. Bus events are Zod-typed and instance-scoped. Key event producers: worktree creation, permission requests, message updates, TUI commands, file changes, session status transitions.
Permission-first design
Section titled “Permission-first design”All agents have explicit permission: PermissionNext.Ruleset defining:
- Tool access (allow/deny per tool).
- Scope (directory patterns — which paths a tool can access).
- Escalation behavior (ask/allow/deny defaults).
- The
planagent denies all edit tools except.opencode/plans/*.md. - The
exploreagent allows only read-only tools.
Streaming responses
Section titled “Streaming responses”Three streaming protocols:
- Session prompt: HTTP stream with
JSON.stringify(message)per chunk. - PTY output: WebSocket binary frames with cursor-based reconnect replay.
- Global events: Server-Sent Events with 10-second heartbeat to prevent proxy stalling.
Async queue synchronization
Section titled “Async queue synchronization”TUI routes use AsyncQueue for request/response coordination between TUI worker and server. The /control/next endpoint blocks until a request is available; /control/response delivers the answer.
Context-based dependency injection
Section titled “Context-based dependency injection”Context.create(name) wraps AsyncLocalStorage for zero-prop-drilling DI. Database transactions are the primary consumer — Database.use() auto-detects transaction context and provides the appropriate client. Instance scoping also uses this pattern.
Zod-everywhere type safety
Section titled “Zod-everywhere type safety”Every API boundary uses Zod schemas: tool parameters, config files, bus events, message parts, provider models. Runtime validation at parse boundaries, TypeScript inference everywhere else. The BusEvent.define() pattern registers events with Zod schemas for both runtime validation and discriminated union generation.
Pitfalls and Hard Lessons (Observed)
Section titled “Pitfalls and Hard Lessons (Observed)”- Tool output can explode context; truncation + external output files are mandatory.
- Provider behavior diverges significantly; one adapter path is insufficient. Each provider needs custom loaders with specific quirks.
- Pending tool calls must be normalized before resend, or providers can reject transcript shape.
- LSP startup and diagnostics timing are inherently flaky; debounce + timeout boundaries are required.
- Compaction and prune are separate controls: summarize conversation vs trim old tool output payloads.
- Permission checks need path-aware semantics (project dir vs worktree vs external filesystem).
- Revert is two-phase (
revertmarker +cleanuphard delete); collapsing these can break UI synchronization and unrevert safety. - Retry timing must respect provider headers first; naive fixed backoff fights rate-limit windows and increases failure loops.
- Token counting is provider-specific: Anthropic includes cached tokens in inputTokens, others exclude them.
- JSON to SQLite migration must handle orphaned records gracefully; batch processing with FK validation is essential.
- File time locking via chained promises prevents concurrent writes but requires all tools to call
FileTime.read()before write. - Snapshot system uses a separate
.gitdirectory per project, not the project’s own git repo. Critical for correct revert. - Dynamic SDK installation needs write locking to prevent concurrent installs of the same package.
- System prompt selection must be model-aware; different model families expect different prompt structures.
- ACP bridge must translate between OpenCode’s rich part types and ACP’s simpler tool_call/text_delta protocol.
CLI Surface (Full Command Tree)
Section titled “CLI Surface (Full Command Tree)”Captured live from the installed binary. Reveals every user-facing feature and most internal subsystems.
Global Options (present on all commands)
Section titled “Global Options (present on all commands)”--port(default: 0) — binds the embedded HTTP server to a random port; non-zero forces a specific port. Shows that evenopencode tuistarts a server.--hostname(default:127.0.0.1) — server bind address.--mdns/--mdns-domain— optional mDNS broadcast for LAN discovery of a running instance. The default domain isopencode.local.--cors— array of additional origins allowed by the Hono CORS middleware.--log-level(DEBUG|INFO|WARN|ERROR) /--print-logs— log subsystem controls.-m, --model— provider/model format string, e.g.anthropic/claude-4-6-sonnet.-c, --continue/-s, --session/--fork— session continuation. Fork creates a branch; continue resumes in place.--prompt/--agent— pre-select a custom prompt or named agent definition.
Top-Level Commands
Section titled “Top-Level Commands”| Command | Purpose |
|---|---|
opencode [project] | Launch full TUI. Default command. |
opencode serve | Headless HTTP server only — no TUI. Same port/hostname/cors flags. |
opencode web | serve + opens browser to web interface. |
opencode acp | Starts ACP (Agent Client Protocol) server. Adds --cwd flag. |
opencode attach <url> | Connect this process as a client to an already-running opencode server. Adds -p, --password (reads from OPENCODE_SERVER_PASSWORD). |
opencode run [message..] | Non-interactive headless agent run. Key for scripting and CI. |
opencode models [provider] | List all available models. --verbose adds cost/metadata. --refresh busts models.dev cache. |
opencode stats | Token usage and cost statistics. --days, --tools, --models, --project filters. |
opencode export [sessionID] | Dump session as JSON. |
opencode import <file> | Restore session from JSON file or share URL. |
opencode pr <number> | Fetch/checkout a GitHub PR branch, then launch TUI. |
opencode upgrade [target] | Self-update. --method selects: curl, npm, pnpm, bun, brew, choco, scoop. |
opencode uninstall | Remove binary + data. --keep-config, --keep-data, --dry-run, --force. |
opencode run — Headless Mode (Important)
Section titled “opencode run — Headless Mode (Important)”opencode run [message..] --command the command to run (use message for args) --format default (pretty) | json (raw JSONL events) --file attach files to message --title session title --attach attach to a running server (http://localhost:4096) --port local server port --share share session --variant model reasoning variant (high, max, minimal) --thinking show thinking blocksThe --format json flag emits raw SSE events as JSONL — this is the machine-readable protocol that the TUI client itself consumes. Critical for understanding the client-server protocol shape.
opencode mcp — MCP Server Management
Section titled “opencode mcp — MCP Server Management”opencode mcp add # add an MCP server (interactive)opencode mcp list # list MCP servers + statusopencode mcp auth [name] # OAuth login for OAuth-enabled MCP server opencode mcp auth list # list OAuth-capable servers + auth stateopencode mcp logout [name] # remove OAuth credentialsopencode mcp debug <name> # debug OAuth connectionExposes that OpenCode manages MCP servers with full OAuth lifecycle (not just stdio).
opencode auth — Provider Credentials
Section titled “opencode auth — Provider Credentials”opencode auth login [url] # log in to a provider; url is optional (for custom endpoints)opencode auth logout # log out from configured provideropencode auth list # list all providersopencode agent — Custom Agent Management
Section titled “opencode agent — Custom Agent Management”opencode agent create # interactive agent creationopencode agent list # list all defined agentsAgents are named configurations of system prompt + model + tool permissions. Managed separately from sessions.
opencode session — Session Management
Section titled “opencode session — Session Management”opencode session list # list all sessionsopencode github — GitHub Integration
Section titled “opencode github — GitHub Integration”opencode github install # install GitHub agent (for PR-level automation)opencode github run # run GitHub agentopencode debug — Internal Diagnostics (Deep)
Section titled “opencode debug — Internal Diagnostics (Deep)”opencode debug config # show fully-resolved config (all layers merged)opencode debug paths # data/config/cache/state global pathsopencode debug scrap # list all known projectsopencode debug skill # list all available skills
opencode debug lsp diagnostics <file> # get LSP diagnostics for a fileopencode debug lsp symbols <query> # search workspace symbolsopencode debug lsp document-symbols <uri> # get symbols from a document
opencode debug rg tree # show file tree via ripgrepopencode debug rg files # list files via ripgrepopencode debug rg search <pattern> # search file contents via ripgrep
opencode debug file read <path> # read file as JSONopencode debug file status # file status infoopencode debug file list <path> # list directoryopencode debug file search <query> # search files by queryopencode debug file tree [dir] # directory tree
opencode debug snapshot track # track current snapshot stateopencode debug snapshot patch <hash> # show patch for snapshot hashopencode debug snapshot diff <hash> # show diff for snapshot hash
opencode debug agent <name> # show agent config detailsopencode debug wait # wait indefinitely (for process attach/debugging)The debug tree mirrors the internal module boundaries exactly: lsp, rg (ripgrep file subsystem), file (VFS abstraction), snapshot, agent, skill, config. This is the most reliable map of OpenCode’s internal subsystem structure.
Next Slice for Following Run
Section titled “Next Slice for Following Run”- Build Codex index (
references/codex) with the same entrypoint->state->leaf tracing depth. - Start converting these index notes into Starlight docs pages with the six-part template from
AGENTS.md.