# anomalyco/opencode

> Client/server TypeScript coding agent on Bun and Effect: any AI SDK provider, permission-ruled tools, TUI, desktop and SDK clients.

- Category: [Open-source coding agents](https://llms-technical-reviews.com/coding-agents/)
- Repository: https://github.com/anomalyco/opencode (reviewed at commit `4ac0d9c3d169bbe81d9570013effdda3fe24d36e`, 2026-10-06)
- Stars: 212017 · Language: TypeScript · License: MIT
- Canonical page: https://llms-technical-reviews.com/p/opencode/

## Overview

OpenCode is an open-source coding agent written in TypeScript on Bun. It is built as a client/server system. The agent itself is a local HTTP server, `packages/opencode`, written with Effect-TS services and storing sessions in SQLite. The terminal UI (SolidJS on OpenTUI), the Electron desktop app, the web app, the IDE integrations and the generated `@opencode-ai/sdk` are all clients of that server. Even the plain `opencode` TUI starts the server in a Bun `Worker` and sends its HTTP requests to it over an in-process RPC bridge ([cli/cmd/tui.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/cli/cmd/tui.ts#L195-L240)).

The agent is provider-agnostic by design. Models come from the models.dev catalog, providers are loaded through Vercel AI SDK packages, and a set of built-in auth plugins adds logins such as GitHub Copilot, Codex (ChatGPT), GitLab and Cloudflare. The loop is the ordinary single-agent kind: one model, tools, repeat until the model stops. The extras sit around it: named agents with permission rulesets, subagents through a `task` tool, git-backed snapshots for undo, and a large plugin hook surface.

## Architecture

```mermaid
flowchart LR
  TUI["TUI (SolidJS / OpenTUI)"] --> SRV["opencode HTTP server"]
  DESK["Desktop / Web / SDK / ACP"] --> SRV
  SRV --> PROMPT["SessionPrompt.runLoop"]
  PROMPT --> TOOLS["SessionTools.resolve"]
  PROMPT --> PROC["SessionProcessor.process"]
  PROC --> LLM["LLM.stream: AI SDK or native runtime"]
  LLM --> PROV["Provider + models.dev catalog"]
  TOOLS --> REG["ToolRegistry: built-in, custom, plugin"]
  TOOLS --> MCP["MCP clients"]
  TOOLS --> PERM["Permission rulesets"]
  PROC --> SNAP["Snapshot (separate git dir)"]
  PROMPT --> COMP["SessionCompaction"]
  PROMPT --> DB["SQLite (Drizzle)"]
```

| Component | Path | Role |
|---|---|---|
| Server | `packages/opencode/src/server/` | HTTP API (Effect `HttpApi`), events, mDNS; what every client talks to |
| Prompt loop | `packages/opencode/src/session/prompt.ts` | `runLoop`: one model step per iteration, subtasks, compaction tasks |
| Processor | `packages/opencode/src/session/processor.ts` | Consumes stream events, records tool parts, doom-loop check, retries |
| LLM seam | `packages/opencode/src/session/llm.ts` | AI SDK `streamText` by default; opt-in native `@opencode-ai/llm` runtime |
| Tools | `packages/opencode/src/tool/` | `read`, `grep`, `glob`, `shell`, `edit`, `write`, `apply_patch`, `task`, `webfetch`, `todo`, `skill`, `lsp` |
| Agents | `packages/opencode/src/agent/agent.ts` | `build`, `plan`, `general`, `explore` plus user agents, each with a ruleset |
| Permissions | `packages/opencode/src/permission/` | Wildcard rules with `allow` / `ask` / `deny` |
| Plugins | `packages/opencode/src/plugin/`, `packages/plugin` | Hook bus, internal auth plugins, npm/local plugins |
| Front ends | `packages/tui`, `packages/desktop`, `packages/web`, `packages/sdk` | Clients of the server API |

## How a request flows

1. **Enter the loop.** `SessionPrompt.loop` wraps `runLoop` in `SessionRunState.ensureRunning`, so one session runs at most one loop ([prompt.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/prompt.ts#L1337-L1347)).
2. **Reload and decide.** Each iteration reloads the session's messages from the database, filtering out compacted history. It exits when the last assistant message has a finish reason other than `tool-calls` or `unknown` and no pending tool parts ([prompt.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/prompt.ts#L1081-L1130)).
3. **Pending tasks first.** A queued subtask runs through `handleSubtask`, and a queued compaction runs `compaction.process`. If the previous step's token count overflows the usable window, a compaction task is created and the loop continues ([prompt.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/prompt.ts#L1141-L1167)).
4. **Build the step.** The loop resolves the agent and its `steps` limit, applies reminders, and creates the assistant message. `SessionTools.resolve` then builds the tool set for this agent, model and permission set ([prompt.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/prompt.ts#L1168-L1240)). The system prompt is environment + instruction files + MCP instructions + skills ([prompt.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/prompt.ts#L1257-L1287)).
5. **Stream.** `SessionProcessor.process` calls `llm.stream` and runs every event through `handleEvent` until the stream ends or compaction is needed. A provider-aware retry policy wraps the whole call ([processor.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/processor.ts#L640-L697)). By default the AI SDK's `streamText` owns provider execution and tool dispatch ([llm.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/llm.ts#L223-L290)).
6. **Tool calls.** Each wrapped tool fires `tool.execute.before`, asks the permission service, executes, and fires `tool.execute.after` ([session/tools.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/tools.ts#L395-L420)). On every `tool-call` event the processor also checks the last three tool parts. If they are identical calls, it raises a `doom_loop` permission request ([processor.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/processor.ts#L330-L381)).
7. **Next step.** The processor returns `continue`, `compact` or `stop`. `continue` loops back to step 2, `compact` creates a compaction and loops, `stop` breaks out. Finally, old tool outputs are pruned in the background ([prompt.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/prompt.ts#L1318-L1335)).

## Key components

### Permissions and agents

`Permission.evaluate` takes the last matching rule across the merged rulesets. When no rule matches, the answer is `ask` ([permission/index.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/permission/index.ts#L28-L38)). The defaults are permissive, though. Every agent starts from `"*": "allow"`, with `ask` only for `doom_loop`, paths outside the project, and `.env` reads. `plan` denies edits except plan files. `explore` denies everything except read, search, web and bash ([agent.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/agent/agent.ts#L118-L215)). Out of the box, then, the `build` agent runs shell commands and edits files without prompting.

### Shell tool

`shell.ts` parses the command with tree-sitter (bash or PowerShell). Every sub-command becomes a permission pattern, with an arity-aware "always" prefix such as `git commit *`. For file commands (`rm`, `cp`, `mv` and others) it resolves path arguments. Any path outside the project triggers an `external_directory` request ([tool/shell.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/tool/shell.ts#L263-L290), [L385-L415](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/tool/shell.ts#L385-L415)). There is no OS sandbox. Isolation is entirely at the permission layer.

### Editing

The registry gives GPT models (except `gpt-4` and `oss` variants) `apply_patch` and everyone else `edit` + `write` ([registry.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/tool/registry.ts#L291-L301)). `edit` is search/replace with a chain of nine fallback matchers, from exact match through line-trimmed, block-anchor (Levenshtein), whitespace-, indentation- and escape-normalised, up to context-aware matching ([edit.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/tool/edit.ts#L694-L704)). After writing, tools run the formatter and report LSP diagnostics back to the model.

### Compaction

`isOverflow` compares total tokens with the model's input limit minus a reserve (by default the smaller of 20k and the max output) ([overflow.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/overflow.ts#L8-L34)). Compaction has two layers. Pruning erases old tool outputs while keeping the latest 40k tokens' worth and never pruning `skill` output. Summarisation then runs a hidden `compaction` agent ([compaction.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/compaction.ts#L28-L31)).

### Snapshots

`Snapshot` keeps its own git directory alongside the work tree (`--git-dir`/`--work-tree`). It takes a tree at step start and a patch at step end, so session revert does not touch the user's own git history.

### Subagents

`task` launches a child session with the named agent. Nesting is capped by `subagent_depth` (default 1). Background subagents sit behind an experimental flag, and child sessions inherit permission denies from the parent ([task.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/tool/task.ts#L97-L150)).

## Extending it

- **Custom tools.** Any `{tool,tools}/*.{js,ts}` file in a config directory is imported, and each export becomes a tool ([registry.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/tool/registry.ts#L183-L197)).
- **Plugins.** Plugins are npm or local modules with hooks such as `tool.execute.before/after`, `shell.env`, `experimental.chat.messages.transform` and `experimental.session.compacting`, plus auth providers. The built-in ones are listed in `internalPlugins` ([plugin/index.ts](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/plugin/index.ts#L66-L86)).
- **MCP.** Servers connect over stdio, SSE or Streamable HTTP, with OAuth. Their tools and resources are exposed to the model and go through the same permission path.
- **Agents, rules and skills.** Custom agents are defined in config. Instructions come from `AGENTS.md`, or from `CLAUDE.md` unless that is disabled, and global and project files are both read. Skills are loaded into the system prompt.
- **Programmatic.** You can use the HTTP API and `@opencode-ai/sdk`, `opencode serve`, `opencode run`, an ACP server, and a GitHub Action handler.

## Running it

- **Install.** Run `curl -fsSL https://opencode.ai/install | bash`, `npm i -g opencode-ai`, or Homebrew. The desktop app is a separate Electron build.
- **Configure a model.** Run `opencode providers login` (alias `auth`) for any models.dev provider, or point an OpenAI-compatible provider at a local server.
- **Run headless.** `opencode serve` starts the API server, and `opencode run "..."` does a one-shot non-interactive run.

## Strengths and caveats

- **Strength: client/server split.** Every UI, including the TUI, is a client of one server API. That makes remote, IDE and scripted use first-class.
- **Strength: provider breadth.** It works with any AI SDK provider plus the models.dev metadata (limits, costs, modalities), with per-family system prompts and provider-specific message fixes.
- **Strength: forgiving edits.** The nine-step matcher chain recovers from the whitespace and indentation slips that make strict search/replace fail.
- **Caveat: permissive defaults, no sandbox.** `"*": "allow"` means bash and edits run unprompted unless you configure rules. Nothing confines commands at the OS level.
- **Caveat: one turn per loop iteration.** Each step reloads history from SQLite and rebuilds the tools and system prompt. It is simple and robust, but it adds work to every model call.
- **Caveat: moving target.** A native LLM runtime, background subagents, code mode and WebSockets are all behind experimental flags, and V1/V2 message schemas coexist in the code.

*Sources: code at 4ac0d9c, verified Q&A.*

## How anomalyco/opencode answers the Open-source coding agents questions

### How is the agent loop implemented? (answered)

## Agent Loop

The agent loop is a **single-loop** architecture — there is no separate Planner; the model calls tools directly until it decides to stop. The main loop is `runLoop` in `packages/opencode/src/session/prompt.ts` (line 1081-1341), which runs inside a `while(true)` controlled by `SessionRunState.ensureRunning` (line 1343-1347). Each iteration (step): loads messages from the database, resolves the model and agent, assembles tools via `SessionTools.resolve`, prepares system prompts (environment, instructions, skills, MCP), and calls `LLM.stream` which delegates to either the native LLM runtime or the AI SDK (`session/llm.ts` lines 226-269). The stream events are processed by `SessionProcessor.process` (`processor.ts` lines 641-697), which handles tool calls, reasoning, text deltas, and step completion via an event handler (`handleEvent`, lines 278-551).

**Tool-call schema** uses the Vercel AI SDK's `Tool` type (`ai` package) — tools emit `tool-input-start/delta/end`, `tool-call`, `tool-result`, and `tool-error` events. Tools execute via `SessionTools.resolve` (`session/tools.ts`), which iterates the `ToolRegistry` to get built-in tools (edit, write, bash, read, grep, glob, task, etc.) and MCP-provided tools. MCP tools' schemas are normalized through `ProviderTransform.schema` (`provider/transform.ts` line 1575).

**Stop conditions**: the loop breaks when the model returns a finish reason other than `"tool-calls"` or `"unknown"` (line 1111-1116), or when the processor returns `"stop"` (line 1319), or when compaction creates a loop (line 1166-1168). If the processor returns `"compact"`, a compaction step is created and the loop continues (line 1321-1328).

**Sub-agents** use the `task` tool (`tool/task.ts`), which spawns a new session with a different agent via `handleSubtask` (`prompt.ts` lines 255-449). Agents like `general`, `explore`, and custom agents are defined in `agent/agent.ts` (lines 140-265) with distinct permission rulesets and system prompts. Background sub-agents use `BackgroundJob` service for async execution.


Citations: [packages/opencode/src/session/prompt.ts:1081-1347](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/prompt.ts#L1081-L1347) · [packages/opencode/src/session/processor.ts:641-697](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/processor.ts#L641-L697) · [packages/opencode/src/session/tools.ts:41-134](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/tools.ts#L41-L134) · [packages/opencode/src/agent/agent.ts:140-265](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/agent/agent.ts#L140-L265) · [packages/opencode/src/tool/task.ts:1-80](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/tool/task.ts#L1-L80) · [packages/opencode/src/session/llm.ts:226-278](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/llm.ts#L226-L278)

### How is repository context gathered and kept within the context window? (answered)

## Context Management

OpenCode gathers repository context through **dedicated tools** rather than embedding a full repo map directly in the prompt. The `read` tool (`tool/read.ts`) reads file contents with optional line ranges. The `grep` tool (`tool/grep.ts`) does regex content search. The `glob` tool (`tool/glob.ts`) does pattern-based file listing. The `list` tool provides directory listings. There is also an MCP resource system that can fetch context from connected servers — `list_mcp_resources`, `list_mcp_resource_templates`, and `read_mcp_resource` (`session/tools.ts` lines 139-386).

**System prompts** are assembled per step in `runLoop` (prompt.ts lines 1257-1270): environment info, agent instructions from `AGENTS.md`-style files (`instruction.ts`), MCP instructions, and skills. The environment prompt in `session/system.ts` (lines 69-80) includes the working directory, workspace root, OS, current date/time, and the model ID. Per-model prompt variants exist for Anthropic, GPT, Gemini, Kimi, Meta, Codex, and a generic default (`session/system.ts` lines 28-51, `session/prompt/` directory).

**Context window overflow** is detected by `isOverflow` in `session/overflow.ts` (lines 8-34), which checks if total tokens exceed the model's context limit minus a reserved buffer (default 20k). The `SessionCompaction` service (`session/compaction.ts`) handles overflow via **pruning** (lines 273-317) — walking backwards through recent tool calls, erasing the output text of older ones (marking `compacted` timestamp) to free space while keeping at least `PRUNE_PROTECT` (40k) tokens protected — and **compaction** (lines 319-557), which uses a dedicated `"compaction"` agent to summarize the conversation history. The compaction strategy selects a `tail_start_id` based on `preserveRecentBudget` (lines 115-119, default 2k-15k tokens reserved for recent turns), and the compaction agent generates a summary that prepends the next model call. If auto-compaction is enabled, after compaction a synthetic `"Continue"` message is injected (line 519-548).

There is **no embedding-based RAG** or vector search in the codebase; context retrieval relies entirely on tool calls (the model decides what to grep/read/glob) and compaction-based summarization of long sessions.


Citations: [packages/opencode/src/session/overflow.ts:8-34](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/overflow.ts#L8-L34) · [packages/opencode/src/session/compaction.ts:273-317](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/compaction.ts#L273-L317) · [packages/opencode/src/session/compaction.ts:319-557](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/compaction.ts#L319-L557) · [packages/opencode/src/session/system.ts:28-80](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/system.ts#L28-L80) · [packages/opencode/src/session/compaction.ts:115-119](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/compaction.ts#L115-L119) · [packages/opencode/src/session/prompt.ts:1257-1270](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/prompt.ts#L1257-L1270)

### How are code edits applied? (answered)

## Code Editing

OpenCode provides **three editing tools**: `edit`, `write`, and `apply_patch`. Which tool is exposed to the model depends on the model ID — `apply_patch` is shown for GPT models while `edit` and `write` are used for others (`tool/registry.ts` lines 297-301).

**Edit tool** (`tool/edit.ts`): Takes `filePath`, `oldString`, `newString`, and optional `replaceAll`. It implements a sophisticated **multi-strategy search/replace** with 9 fallback replacers: `SimpleReplacer` (exact match), `LineTrimmedReplacer` (trimmed lines), `BlockAnchorReplacer` (first/last line anchors with Levenshtein similarity), `WhitespaceNormalizedReplacer`, `IndentationFlexibleReplacer`, `EscapeNormalizedReplacer`, `TrimmedBoundaryReplacer`, `ContextAwareReplacer`, and `MultiOccurrenceReplacer` (lines 694-704). Each replacer is tried in order until a unique match is found. The tool computes a unified diff before applying, shows the diff in permission prompts, normalizes line endings, handles BOM, and formats the file after writing. If the edited file has LSP diagnostics errors, they're reported back to the model (lines 196-201). A semaphore-based file lock prevents concurrent edits (lines 35-45).

**Write tool** (`tool/write.ts`): Takes `filePath` and `content`. Computes a diff against the existing file (or empty if new), shows it in a permission prompt, writes the file with directory creation, formats it, and reports LSP diagnostics (up to 5 files).

**Apply Patch tool** (`tool/apply_patch.ts`): Takes a full `patchText` in a custom patch format. Validates the patch, parses hunks (supports add, update, delete, move), verifies each file path against external directory checks, computes per-file diffs, asks for permission with the total diff, then applies all changes. After applying, runs LSP diagnostics across all touched files (lines 266-293).

**Validation** comes from LSP diagnostics after each write/edit operation — if LSP errors are found, the output tells the model to fix them. There is no automated test-run or lint-run after editing in the editing tools themselves (that's left to the model's discretion or a subsequent step).

**Undo/git integration**: The `Snapshot` service (`snapshot/index.ts`) tracks file patches via `structuredPatch` (diff library) keyed by a hash. Before starting LLM stream execution, a snapshot is captured (`track()`, line 102 in processor.ts). On step finish, the snapshot is compared and a `patch` part is persisted (processor.ts lines 471-483). The `Session.revert` field stores snapshot/diff info for undo operations (`session.ts` lines 209-214, `session/revert.ts`).


Citations: [packages/opencode/src/tool/edit.ts:682-730](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/tool/edit.ts#L682-L730) · [packages/opencode/src/tool/edit.ts:244-426](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/tool/edit.ts#L244-L426) · [packages/opencode/src/tool/write.ts:38-104](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/tool/write.ts#L38-L104) · [packages/opencode/src/tool/apply_patch.ts:30-310](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/tool/apply_patch.ts#L30-L310) · [packages/opencode/src/tool/registry.ts:291-301](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/tool/registry.ts#L291-L301) · [packages/opencode/src/snapshot/index.ts:1-60](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/snapshot/index.ts#L1-L60)

### How are shell commands and file writes kept safe? (answered)

## Execution Safety

OpenCode uses a **ruleset-based permission system** with wildcard pattern matching. The `Permission` service (`permission/index.ts`) evaluates each tool call against a stack of rulesets. Rules have `action` in `{"allow", "deny", "ask"}`. For each tool execution, `evaluate()` (line 28-38) scans rulesets in reverse order and returns the first matching rule (defaulting to `"ask"`). If the action is `"allow"`, execution proceeds silently. If `"deny"`, a `DeniedError` is thrown. If `"ask"`, a pending permission request is created and the user must reply (`ask()`, line 67-107). Approvals can be `"once"` or `"always"` (stored in the session's `approved` list for future auto-allow). Denials can include user feedback as a `CorrectedError` (line 125).

**Permission rulesets** are defined per-agent in `agent/agent.ts`. The `build` agent (default) allows most tools but `"ask"` for `doom_loop`, external directories, and `.env` file reads (lines 119-155). The `plan` agent denies all edit/write tools except plan files (lines 156-181). The `explore` agent denies everything except read-only tools (grep, glob, list, bash, read, webfetch, websearch) (lines 197-213). Custom agents and user-configured permissions merge on top of these defaults (lines 267-294).

**Shell command safety** (`tool/shell.ts`): The shell tool detects dangerous command types — it has sets for `CWD` (cd, pushd etc.), `FILES` (rm, cp, mv, chmod, chown, etc.), and potentially destructive commands. Output is truncated via the `Truncate` service. Timeouts are configurable (default 300s).

**External directory checks**: The `assertExternalDirectoryEffect` function (`tool/external-directory.ts`) prevents writing outside the project worktree unless explicitly allowed by the permission ruleset.

**Doom loop detection** in `processor.ts` (lines 353-381) checks if the same tool has been called with the same input 3+ times consecutively, and requires explicit user permission to continue.

**Sandboxing/containers**: There is no built-in container sandbox or Landlock/Seccomp integration visible in the codebase. Execution safety relies on the permission system operating at the tool-call level, not OS-level isolation. The `Question` tool provides structured input from the user. The codebase does not implement network egress filtering or allow/deny lists for network access beyond what the permission system covers.

**Checkpoints via Snapshot** (`snapshot/index.ts`): Before each LLM step, a git-based snapshot is taken (`track()`). On step completion, the diff is captured (`patch()`). The revert system allows rolling back snapshots.

> **Editor's note.** Correction: the shell tool has no dangerous-command detector; its CWD/FILES sets only find paths outside the project for `external_directory` prompts, and each tree-sitter-parsed sub-command becomes a `bash` permission pattern. The default ruleset is `"*": "allow"`, so shell and edits run unprompted unless configured.

Citations: [packages/opencode/src/permission/index.ts:28-174](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/permission/index.ts#L28-L174) · [packages/opencode/src/agent/agent.ts:119-265](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/agent/agent.ts#L119-L265) · [packages/opencode/src/session/processor.ts:353-381](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/processor.ts#L353-L381) · [packages/opencode/src/tool/shell.ts:1-60](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/tool/shell.ts#L1-L60) · [packages/opencode/src/snapshot/index.ts:1-60](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/snapshot/index.ts#L1-L60) · [packages/opencode/src/agent/agent.ts:138-155](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/agent/agent.ts#L138-L155)

### Which models are supported and how are they called? (answered)

## Model Support

OpenCode uses the **Vercel AI SDK** (`ai` package) as its primary LLM abstraction layer, with an opt-in native LLM runtime (`@opencode-ai/llm`). The `Provider` service (`provider/provider.ts`) loads model configurations from a catalog (`@opencode-ai/core/models-dev.ts`), which defines models with fields for context limits, output limits, cost tiers, reasoning options, modalities (text/image/audio/pdf), temperature support, tool-call support, and per-provider npm packages.

**Providers** are resolved from the catalog and configured via `@ai-sdk/*` npm packages. The `sdkKey()` function in `provider/transform.ts` (lines 41-98) maps npm packages to AI SDK provider option keys: `@ai-sdk/anthropic`, `@ai-sdk/openai`, `@ai-sdk/google`, `@ai-sdk/amazon-bedrock`, `@ai-sdk/mistral`, `@ai-sdk/groq`, `@ai-sdk/xai`, `@ai-sdk/cohere`, `@ai-sdk/perplexity`, `@ai-sdk/togetherai`, `@ai-sdk/azure`, `@ai-sdk/alibaba`, `@ai-sdk/cerebras`, `@ai-sdk/deepinfra`, `@ai-sdk/vercel`, `@ai-sdk/gateway`, plus venice, openrouter, gitlab-ai-provider, and OpenAI-compatible providers via `@ai-sdk/openai-compatible`. Plugins add auth support for GitHub Copilot, Codex, GitLab, Poe, Cloudflare, Azure, DigitalOcean, Snowflake Cortex, xAI, Cerebras, and Modal.

**Local models** are supported via OpenAI-compatible endpoints (`@ai-sdk/openai-compatible`), enabling any local LLM server (like Ollama, LM Studio, or vLLM) that exposes an OpenAI-compatible API.

**Tool-calling vs text formats**: Models with `tool_call: true` in the catalog definition get full tool definitions. The AI SDK handles tool-calling natively; OpenCode normalizes the stream events through `LLMAISDK.toLLMEvents` (`session/llm/ai-sdk.ts`). For providers without tool-calling, the model would only produce text.

**Per-model prompt tuning** is in `session/system.ts` (lines 28-51): Anthropic models get one system prompt (`anthropic.txt`), GPT-4/o1/o3 get `beast.txt`, newer GPTs get `astra.txt` or `codex.txt`, Gemini gets `gemini.txt`, Kimi gets `kimi.txt`, Meta/Muse gets `meta.txt`, and everything else gets `default.txt`.

**Provider-specific message transforms** in `provider/transform.ts` handle quirks: Anthropic models filter empty content parts and normalize tool call IDs (lines 170-194, 224-251), Bedrock uses signature-based reasoning, Mistral pads/scrambles tool call IDs (lines 253-277), and OpenAI uses `include` for encrypted reasoning (line 23).

**Cost tracking** uses `Session.getUsage()` (`session.ts` lines 338-405), which reads cost tiers from the model catalog, handles cache read/write pricing, and accounts for provider-specific metadata keys (Anthropic vertex, Bedrock, Venice).


Citations: [packages/opencode/src/provider/transform.ts:41-98](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/provider/transform.ts#L41-L98) · [packages/opencode/src/session/system.ts:28-51](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/system.ts#L28-L51) · [packages/opencode/src/provider/transform.ts:100-280](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/provider/transform.ts#L100-L280) · [packages/opencode/src/session/session.ts:338-405](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/session.ts#L338-L405) · [packages/opencode/src/provider/provider.ts:1-33](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/provider/provider.ts#L1-L33) · [packages/opencode/src/session/llm.ts:226-278](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/llm.ts#L226-L278)

### How can it be extended and customised? (answered)

## Extensibility

**MCP (Model Context Protocol)** is fully integrated via `packages/opencode/src/mcp/`. The `MCP.Service` manages MCP client connections supporting stdio, SSE, and Streamable HTTP transports (`mcp/index.ts` lines 1-80). MCP servers are configured in the user config and launched as child processes or connected via URL. MCP tools are automatically discovered and exposed to the LLM alongside built-in tools. The `SessionTools.resolve` function (`session/tools.ts` lines 390-491) wraps each MCP tool with permission checking, truncation, and event publishing. MCP resources can be listed and read via dedicated LLM tools (`list_mcp_resources`, `read_mcp_resource`, etc., lines 139-386).

**Plugin system** (`plugin/index.ts`): OpenCode loads plugins via two mechanisms: 1) **Internal plugins** — provider-specific auth plugins directly imported (Codex, Copilot, GitLab, Poe, Cloudflare, Azure, DigitalOcean, xAI, Cerebras, Snowflake Cortex, Modal) — see `internalPlugins()` (lines 67-86). 2) **External plugins** from npm or local files via the `PluginLoader`. Plugins can provide custom tools (registered via `tool:` hooks in the plugin module), extend system prompts, transform messages, modify tool executions, and hook into session lifecycle events. The `Plugin.trigger()` function fires hooks at named points like `experimental.chat.system.transform`, `tool.execute.before`, `tool.execute.after`, `experimental.session.compacting`, and `experimental.text.complete` (examples throughout `processor.ts` and `prompt.ts`).

**Custom tools** can be defined by placing `{js,ts}` files in `{tool,tools}/` directories within config directories (`tool/registry.ts` lines 183-197). Each file exports tool definitions with `args`, `description`, and `execute` fields, which are automatically loaded and registered.

**Rules/instruction files**: The `Instruction` service loads instructions from `AGENTS.md` files in the project (the repo's `AGENTS.md` at root works as a per-repo instruction file). Skills are defined in `.claude/sk.json` or similar config.

**Hooks** are implemented via the `Plugin` system — the `Hooks` interface includes lifecycle callbacks for chat system transforms, message transforms, tool definitions, compaction, and more. Plugins can also act as **Workspace Adapters** via `registerAdapter` (`control-plane/adapters/`).

**Headless/SDK use**: OpenCode exports the `@opencode-ai/sdk` package (`packages/sdk/`) and `@opencode-ai/plugin` for programmatic use and plugin development. The `opencode-ai` npm package is designed for headless/CI use (`curl -fsSL https://opencode.ai/install`). The architecture separates core services (schema, protocol, core) from UI (TUI, web, desktop), enabling headless/server-mode operation.

> **Editor's note.** Correction: skills are not defined in `.claude/sk.json`; they are directories with a `SKILL.md`, discovered by packages/opencode/src/skill/discovery.ts.

Citations: [packages/opencode/src/mcp/index.ts:1-80](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/mcp/index.ts#L1-L80) · [packages/opencode/src/session/tools.ts:139-491](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/session/tools.ts#L139-L491) · [packages/opencode/src/plugin/index.ts:1-100](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/plugin/index.ts#L1-L100) · [packages/opencode/src/tool/registry.ts:183-253](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/tool/registry.ts#L183-L253) · [packages/opencode/src/agent/agent.ts:267-310](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/agent/agent.ts#L267-L310) · [packages/opencode/src/control-plane/adapters/index.ts:1-10](https://github.com/anomalyco/opencode/blob/4ac0d9c3d169bbe81d9570013effdda3fe24d36e/packages/opencode/src/control-plane/adapters/index.ts#L1-L10)
