OpenCode

https://github.com/anomalyco/opencode eb553f5
MIT TypeScript linuxmacoswindows
Confidence levels high Confirmed in source · medium Inferred from patterns · low Best guess

Core Metadata

License high

MIT

"license": "MIT"
Primary Language medium

TypeScript

"typecheck": "tsgo --noEmit"
"./*": "./src/*.ts"
Platforms high

linux, macos, windows

scoop install opencode # Windows
brew install anomalyco/tap/opencode # macOS and Linux
sudo pacman -S opencode # Arch Linux

Edit

Mechanism high

apply_patch, str_replace, rewrite_file

Mechanism Default medium

apply_patch

if (t.id === "apply_patch") return usePatch
if (t.id === "edit" || t.id === "write") return !usePatch
Anchoring high

`apply_patch` anchors updates using `@@` sections plus context/old-line sequence matching; `edit` falls back through block/context/whitespace/indentation matchers.

if (lines[i].startsWith("@@"))
const contextIdx = seekSequence(originalLines, [chunk.change_context], lineIndex)
for (const replacer of [
Verification high

Reject-on-mismatch. Patch input is parsed and verified before writes; edit fails when oldString is missing or non-unique.

throw new Error(`apply_patch verification failed: ${error}`)
throw new Error("apply_patch verification failed: no hunks found")
Could not find oldString in the file. It must match exactly, including whitespace, indentation, and line endings.
Found multiple matches for oldString
Retry Loop medium

No external auto-retry loop for failed edits; each call is single-shot. `edit` performs internal matcher fallbacks before failing.

for (const replacer of [
async execute(params, ctx)
Failure Modes high
  • patch_parse_or_empty Invalid patch envelope or empty patch is rejected.
  • patch_target_missing Update fails if target file cannot be read.
  • patch_context_mismatch Update fails when expected context/old lines are not found.
  • edit_old_string_not_found Exact/fallback search cannot find target text.
  • edit_ambiguous_match Multiple matches without replaceAll are rejected.
throw new Error("patch rejected: empty patch")
Failed to read file to update
Failed to find expected lines
Could not find oldString in the file
Found multiple matches for oldString

Tools

Available high

bash, read, list, glob, grep, edit, write, apply_patch, task, skill, todowrite, webfetch, websearch, codesearch, question, lsp (experimental), custom tools from config dirs/plugins, MCP tools

const glob = new Bun.Glob("{tool,tools}/*.{js,ts}")
for (const [key, item] of Object.entries(await MCP.tools()))
Schema Style high

Zod-defined parameters converted to JSON Schema for AI SDK function tools; MCP tools converted from MCP JSON schema; one freeform-style patch payload field (`patchText`).

z.toJSONSchema(item.parameters)
inputSchema: jsonSchema(schema as any)
return dynamicTool({
Sandbox medium

No hard OS sandbox is implemented in tool runner. Boundaries are permission rules (`allow`/`ask`/`deny`) plus explicit `external_directory` checks; shell runs via local spawn.

`"allow"` — run without approval
return match ?? { action: "ask", permission, pattern: "*" }
permission: "external_directory"
const proc = spawn(params.command, {

Config

Locations high

remote org config: <provider>/.well-known/opencode, global config: ~/.config/opencode/opencode.json|jsonc, custom config file: $OPENCODE_CONFIG, project config: opencode.json|jsonc (searches upward), .opencode directories (agents/commands/plugins/etc.), custom config dir: $OPENCODE_CONFIG_DIR, inline config: $OPENCODE_CONFIG_CONTENT, enterprise managed config: /etc/opencode (Linux), /Library/Application Support/opencode (macOS), %ProgramData%\opencode (Windows)

1. **Remote config** (from `.well-known/opencode`) - organizational defaults
Config loading order (low -> high precedence)
if (Flag.OPENCODE_CONFIG)
Filesystem.findUp(file, Instance.directory, Instance.worktree)
if (Flag.OPENCODE_CONFIG_DIR)
if (Flag.OPENCODE_CONFIG_CONTENT)
return "/Library/Application Support/opencode"
Env Vars high
  • name: OPENCODE_CONFIG meaning: Path to custom config file loaded between global and project configs.
  • name: OPENCODE_CONFIG_DIR meaning: Additional config directory scanned for agents/commands/modes/plugins.
  • name: OPENCODE_CONFIG_CONTENT meaning: Inline JSON config override with highest non-managed precedence.
  • name: OPENCODE_PERMISSION meaning: JSON override merged into permission rules.
  • name: OPENCODE_DISABLE_PROJECT_CONFIG meaning: Disable project config discovery/scanning.
  • name: OPENCODE_DISABLE_AUTOCOMPACT meaning: Forces `compaction.auto=false`.
  • name: OPENCODE_DISABLE_PRUNE meaning: Forces `compaction.prune=false`.
  • name: OPENCODE_ENABLE_EXA meaning: Enables Exa-backed `websearch`/`codesearch` outside OpenCode provider.
Specify a custom config file path using the `OPENCODE_CONFIG` environment variable.
Specify a custom config directory using the `OPENCODE_CONFIG_DIR`
export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"]
export const OPENCODE_PERMISSION = process.env["OPENCODE_PERMISSION"]
Object.defineProperty(Flag, "OPENCODE_DISABLE_PROJECT_CONFIG"
if (Flag.OPENCODE_DISABLE_AUTOCOMPACT)
if (Flag.OPENCODE_DISABLE_PRUNE)
export const OPENCODE_ENABLE_EXA =

Extensions

Supported high

true

Skills are loaded on-demand via the native `skill` tool
Locations high

.opencode/skills/<name>/SKILL.md, ~/.config/opencode/skills/<name>/SKILL.md, .claude/skills/<name>/SKILL.md, ~/.claude/skills/<name>/SKILL.md, .agents/skills/<name>/SKILL.md, ~/.agents/skills/<name>/SKILL.md, additional configured paths (`skills.paths`), remote URL indexes (`skills.urls`)

Project config: `.opencode/skills/<name>/SKILL.md`
Project agent-compatible: `.agents/skills/<name>/SKILL.md`
for (const skillPath of config.skills?.paths ?? [])
for (const url of config.skills?.urls ?? [])
Format high

Markdown `SKILL.md` with YAML frontmatter (`name`, `description` required).

Each `SKILL.md` must start with YAML frontmatter.
Plugins high

Supported. Loads local TS/JS plugin files and npm plugins, auto-installs dependencies with Bun, and exposes hook/event APIs plus plugin-defined tools.

`.opencode/plugins/` - Project-level plugins
Specify npm packages in your config file.
**npm plugins** are installed automatically using Bun at startup.
export async function trigger<
for (const [id, def] of Object.entries(plugin.tool ?? {}))
Mcp high

Supported MCP client for local+remote servers, OAuth flows, dynamic MCP tool registration, and MCP prompts/resources consumption.

OpenCode supports both local and remote servers.
OpenCode automatically handles OAuth authentication for remote MCP servers.
export async function tools()
export async function prompts()
export async function resources()

Providers

Supported medium

models.dev provider catalog (75+ providers), opencode, anthropic, openai, amazon-bedrock, azure, google, google-vertex, google-vertex-anthropic, openrouter, xai, mistral, groq, deepinfra, cerebras, cohere, gateway, togetherai, perplexity, vercel, gitlab, github-copilot, github-copilot-enterprise

support **75+ LLM providers** and it supports running local models.
const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
Auth high

Supports API keys, OAuth tokens, and well-known tokens; credentials are stored in `~/.local/share/opencode/auth.json`; `/connect` and `opencode auth` workflows are documented.

in `~/.local/share/opencode/auth.json`.
export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown])
Model Selection high

Model can be selected by CLI flag (`--model`), config (`model`/`small_model`), and interactive `/models`; default selection falls back to recent-model state then provider defaults.

describe: "model to use in the format of provider/model",
if (cfg.model) return parseModel(cfg.model)
path.join(Global.Path.state, "model.json")

Ux

Interface high

CLI with interactive TUI by default, non-interactive run mode, and optional headless server.

The OpenCode CLI by default starts the [TUI](/docs/tui)
opencode run "Explain how closures work in JavaScript"
`opencode serve` command runs a headless HTTP server
Streaming high

Yes. `opencode run --format json` streams structured events; server exposes SSE event streams.

"format: default (formatted) or json (raw JSON events)"
if (args.format === "json") {
`GET` | `/global/event` | Get global events (SSE stream)
Review Controls high

Permission system supports `allow`/`ask`/`deny` with prompt-time approvals (`once`, `always`, `reject`) and per-agent overrides.

`"allow"` — run without approval
`once` — approve just this request
export const Reply = z.enum(["once", "always", "reject"])
Agent permissions are merged with the global config
Sessions high

Session/message state is persisted in SQLite at `~/.local/share/opencode/opencode.db`; CLI supports continue/session/fork flows.

export const Path = path.join(Global.Path.data, "opencode.db")
`--continue` | `-c` | Continue the last session
describe: "fork the session before continuing (requires --continue or --session)",

Context

Compaction high

Automatic compaction is token-threshold based (`reserved` buffer) and configurable (`compaction.auto`, `compaction.prune`, `compaction.reserved`); compaction can auto-insert a synthetic continue message.

if (config.compaction?.auto === false) return false
config.compaction?.reserved ?? Math.min(COMPACTION_BUFFER, ProviderTransform.maxOutputTokens(input.model))
`auto` - Automatically compact the session when context is full
if (result === "continue" && input.auto)
Overflow Handling high

When near context limit, session loop enqueues compaction and continues; context-overflow errors are treated as non-retryable.

// context overflow, needs compaction
await SessionCompaction.create({
context overflow errors should not be retried

Reliability

Auto Retry medium

Automatic retry is enabled for retryable provider/API failures with exponential backoff and `Retry-After` header support; no explicit user-facing disable switch was found in scanned docs/code.

const retry = SessionRetry.retryable(error)
await SessionRetry.sleep(delay, input.abort)
const retryAfterMs = headers["retry-after-ms"]
RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1)
Recovery Loops high

Includes a doom-loop guard (same tool call repeated 3 times) that requests permission, plus auto-compaction loop handoff (`process` returns `compact`).

const DOOM_LOOP_THRESHOLD = 3
await PermissionNext.ask({
if (needsCompaction) return "compact"

Integration

Sdk high

Supported JS/TS SDK package `@opencode-ai/sdk` (createOpencode server+client or createOpencodeClient to attach).

npm install @opencode-ai/sdk
import { createOpencode } from "@opencode-ai/sdk"
import { createOpencodeClient } from "@opencode-ai/sdk"
Modes high

Interactive TUI, non-interactive `run`, headless HTTP `serve`, and ACP subprocess mode.

The OpenCode CLI by default starts the [TUI](/docs/tui)
command: "run [message..]"
`opencode serve` command runs a headless HTTP server
Protocol high

HTTP/OpenAPI server APIs with SSE event streams; ACP uses JSON-RPC over stdio with NDJSON framing.

client that talks to the server. The server exposes an OpenAPI 3.1 spec
Get global events (SSE stream)
communicates with your editor over JSON-RPC via stdio
const stream = ndJsonStream(input, output)

Customization

System Prompt high

System instructions come from agent prompt + built-in model prompt, project/global `AGENTS.md` (with `CLAUDE.md` compatibility), and `instructions` files/URLs in config.

You can provide custom instructions to opencode by creating an `AGENTS.md` file.
1. **Local files** by traversing up from the current directory (`AGENTS.md`, `CLAUDE.md`)
// use agent prompt otherwise provider prompt
Prompt Templates high

Prompt templates are defined as custom commands: JSON `command.<name>.template` or Markdown files in `commands/**/*.md` with frontmatter + body template.

The frontmatter defines command properties. The content becomes the template.
const COMMAND_GLOB = new Bun.Glob("{command,commands}/**/*.md")
Keybindings high

Configurable via `opencode.json` `keybinds` object (string key chords, `none` to disable).

you can customize through the OpenCode config
You can disable a keybind by adding the key to your config with a value of "none".
keybinds: Keybinds.optional().describe("Custom keybind configurations")
Themes high

Theme selection via config (`theme`) with built-ins and JSON custom themes loaded from user/project `.opencode/themes` directories.

Themes are loaded from multiple directories
`~/.config/opencode/themes/*.json`
`<project-root>/.opencode/themes/*.json`
theme: z.string().optional().describe("Theme name to use for the interface")

Distribution

Artifacts high

install script (`curl ... | bash`), npm global package (`opencode-ai`), Homebrew formula, Scoop and Chocolatey packages, Arch Linux and AUR packages, Nix package, desktop binaries on GitHub releases

curl -fsSL https://opencode.ai/install | bash
npm i -g opencode-ai@latest
brew install anomalyco/tap/opencode
Download directly from the [releases page](https://github.com/anomalyco/opencode/releases)

Packages

Install Locations high

$OPENCODE_INSTALL_DIR, $XDG_BIN_DIR, $HOME/bin, $HOME/.opencode/bin

The install script respects the following priority order
1. `$OPENCODE_INSTALL_DIR`
4. `$HOME/.opencode/bin`

Extras

tools.model_conditional_exposure high

Tool registry toggles `apply_patch` versus `edit`/`write` exposure based on model ID checks.

model.modelID.includes("gpt-") && !model.modelID.includes("oss") && !model.modelID.includes("gpt-4")
if (t.id === "apply_patch") return usePatch
if (t.id === "edit" || t.id === "write") return !usePatch
integration.local_rpc_mode high

TUI worker thread supports direct RPC communication without HTTP.

// Use direct RPC communication (no HTTP)
function createWorkerFetch(client: RpcClient): typeof fetch {
reliability.loop_guard_doom_loop high

Session processing uses a doom-loop threshold of three repeated tool calls before asking permission.

const DOOM_LOOP_THRESHOLD = 3
await PermissionNext.ask({
config.variable_substitution high

Config interpolation replaces `{env:...}` variables and resolves `{file:...}` references.

text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
const fileMatches = text.match(/\{file:[^}]+\}/g)