Search for a command to run...
Compiled from 23 nodes · est. 69 min read
Updated
OpenCode is an open-source AI coding agent delivered through three surfaces: a terminal-based TUI, an Electron/SolidJS desktop app, and an IDE extension speaking the Agent Client Protocol (ACP). A shared Core backend handles provider routing, session management, MCP tool integrations, subagents, and permissions; the Desktop and TUI are thin surfaces over that engine. OpenCode connects to 75+ LLM providers via the AI SDK and Models.dev, and exposes a programmatic client through the @opencode-ai/sdk npm package. Two server protocol generations coexist in the codebase — referred to as v1 (legacy) and v2 (current) — and the Desktop v2 layout migration was completed at release v1.18.0.
The Getting started section covers orientation, installation, local development, and contributing, and is the right entry point for most readers. The CLI and API section documents the yargs-based CLI entry point and the OpenAPI 3.1 HTTP server, while SDK and embedding covers the @opencode-ai/sdk npm package for programmatic control. The Configuration and plugins section groups the Configuration, MCP integration, Plugins, and Upgrading pages — everything about extending or customizing OpenCode from the outside. The Agents and sessions section covers Agents and skills, Session lifecycle, and Permissions, which together describe how a conversation runs, what tools it can call, and how approvals work. The Tools and Providers and LLM sections describe the LLM-facing surface: how tools are defined, registered, and executed, and how requests are built and streamed to providers like Bedrock, Azure, and Google Vertex. The CI and testing section documents the test workflow, toolchain caching, and unit and e2e test layout.
If you want to install OpenCode and try it, read Installation and then CLI commands. If you want to understand the architecture before touching code, read Orientation under Getting started, then Server and HTTP API, then Session lifecycle. If you want to extend OpenCode with a new tool, MCP server, or hook, read Plugins, MCP integration, and the Tools section. If you are contributing a fix or feature, read Local development and Contributing first, then CI and testing before opening a PR.
Updated
Pages in this section:
Updated
OpenCode is a multiplatform AI coding agent with three deployment shapes (TUI, desktop, IDE extension) sharing a core server in packages/opencode and web UI in packages/app, orchestrated via Bun workspaces across the packages/ directory tree. The codebase uses runtime-conditional database adapters (Bun vs. Node) and publishes ESM modules under MIT, with CLI entry point at ./bin/opencode and IDE integration via the Agent Client Protocol.
OpenCode is an open-source AI coding agent available as a terminal-based TUI, a desktop app, and an IDE extension.[1] OpenCode supports the Agent Client Protocol (ACP), enabling use directly inside compatible editors and IDEs without the TUI.[2]
The root package.json uses Bun workspaces covering all directories under packages/*, packages/console/*, packages/stats/*, packages/sdk/js, and packages/slack.[3] packages/opencode contains the core business logic and server; its src/cli/cmd/tui/ subdirectory holds the TUI, written in SolidJS with opentui.[4] packages/app holds the shared web UI components written in SolidJS, and packages/desktop contains the native desktop app built with Electron, which wraps packages/app.[4] packages/plugin is the source for the @opencode-ai/plugin npm package.[4]
The opencode package is published under the MIT license as an ESM module ("type": "module").[5] The opencode CLI binary entry point is ./bin/opencode, declared in packages/opencode/package.json.[5] packages/opencode/package.json uses a conditional #db import map to select the database adapter: ./src/storage/db.bun.ts for Bun environments and ./src/storage/db.node.ts for Node, defaulting to the Bun adapter.[5]
Sources
Updated
OpenCode is installable via one-line script, multiple package managers (npm, Homebrew, Scoop, Chocolatey, etc.), and Docker, with platform-specific recommended methods: Homebrew on macOS/Linux, Chocolatey/Scoop/WSL on Windows. The desktop app and TUI require compatible terminal emulators (WezTerm, Alacritty, Ghostty, Kitty) or run as a native application on macOS, Windows, and Linux.
OpenCode can be installed via a one-line script (curl -fsSL https://opencode.ai/install | bash), package managers (npm, Bun, pnpm, or Yarn under the package name opencode-ai), Homebrew, Scoop, Chocolatey, Pacman, AUR, Mise, Nix, or Docker.[1] Versions older than 0.1.x must be removed before installing.[2]
On macOS and Linux, Homebrew (brew install anomalyco/tap/opencode) is the recommended package-manager install, as it is always up to date.[2] On Arch Linux, OpenCode is available via sudo pacman -S opencode (stable) or paru -S opencode-bin (latest from AUR).[1] On Windows, supported installers include Chocolatey (choco install opencode), Scoop (scoop install opencode), npm (npm install -g opencode-ai), Mise (mise use -g github:anomalyco/opencode), and Docker (docker run -it --rm ghcr.io/anomalyco/opencode); Bun support on Windows is in progress.[1] For the best experience on Windows, installing via Windows Subsystem for Linux (WSL) is recommended for better performance and full feature compatibility.[1]
The install script selects an installation directory using this priority order: $OPENCODE_INSTALL_DIR → $XDG_BIN_DIR → $HOME/bin → $HOME/.opencode/bin (default fallback).[2]
The OpenCode desktop app is available for macOS (Apple Silicon and Intel), Windows (x64), and Linux (.deb, .rpm, .AppImage), downloadable from the releases page or opencode.ai/download.[2]
The recommended terminal emulators for OpenCode's TUI are WezTerm (cross-platform), Alacritty (cross-platform), Ghostty (Linux and macOS), and Kitty (Linux and macOS).[1] OpenCode is available in the Zed ACP Registry and can be installed directly by running zed: acp registry in the Zed Command Palette, without needing manual configuration.[3]
Sources
Updated
OpenCode local development uses Bun (≥1.3) as its package manager; start development with bun install && bun dev, which runs the CLI from packages/opencode against the current directory or a specified path. The dev setup comprises multiple entry points: bun dev for the CLI, bun dev serve for the headless API on port 4096, bun run --cwd packages/app dev for the web UI (port 5173), and bun run --cwd packages/desktop dev for the desktop app, plus debugging via bun run --inspect.
Local development requires Bun 1.3+; the root package.json pins the package manager to bun@1.3.14.[1][2] Install dependencies and start the dev server from the repo root with bun install && bun dev.[1] After every bun install, the postinstall script in package.json automatically runs fix-node-pty in packages/core.[2] The following dependencies are listed as trusted in package.json, meaning their install scripts are allowed to run: esbuild, node-pty, protobufjs, tree-sitter, tree-sitter-bash, tree-sitter-powershell, web-tree-sitter, and electron.[2]
The root dev script in package.json runs the opencode CLI as bun run --cwd packages/opencode --conditions=browser src/index.ts; the per-package equivalent in packages/opencode/package.json is bun run --conditions=browser ./src/index.ts.[2][3] During development, bun dev is the local equivalent of the built opencode command, exposing the same CLI interface including serve, web, and directory arguments.[1] By default, bun dev runs opencode in the packages/opencode directory; pass a directory path (e.g., bun dev .) to run it against a different location.[1] The --conditions=browser flag passed in the dev script instructs Bun to resolve package exports using the browser condition rather than node, targeting the correct runtime environment for OpenCode's bundling.
bun dev serve starts the headless API server on port 4096 by default; a different port can be specified with --port.[1] To test UI changes, first start the opencode server with bun dev serve, then run bun run --cwd packages/app dev; the web dev server starts at http://localhost:5173.[[1]](https://github.com/anomalyco/opencode/blob/dc4449df0d52199704ea4989a5a993ebbc605612/CONTRIBUTING.md) After changing the API or SDK (e.g., packages/opencode/src/server/server.ts), run ./script/generate.ts to regenerate the SDK and related files.[1]
The desktop app runs in development with bun run --cwd packages/desktop dev; production builds use the build then package subcommands in the same package.[1]
To build a standalone executable, run ./packages/opencode/script/build.ts --single (also invokable as bun run script/build.ts via the build script in packages/opencode/package.json), then execute ./packages/opencode/dist/opencode-<platform>/bin/opencode.[1][3]
The most reliable way to debug opencode is to run bun run --inspect=<url> dev ... in a terminal and attach the debugger via that URL; other methods such as VSCode launch configs or the JS Debug Terminal can cause breakpoints to be mapped incorrectly.[1] When debugging with breakpoints in server code while running the TUI, use bun dev spawn instead of bun dev; the default bun dev runs the server in a worker thread where breakpoints may not fire.[1] To debug the server and TUI separately, start the server with bun run --inspect=ws://localhost:6499/ --cwd packages/opencode ./src/index.ts serve --port 4096, then attach the TUI with opencode attach http://localhost:4096.[1]
The repo uses oxlint for linting, runnable via bun run lint.[2] Type-checking across the monorepo is run with bun turbo typecheck (via bun run typecheck); within packages/opencode, type-checking uses tsgo --noEmit (the TypeScript native-preview binary), not tsc.[2][3] Running bun test from the repo root is intentionally blocked — the root test script in package.json prints an error and exits with code 1.[2] The test command in packages/opencode/package.json is bun test --timeout 30000 --only-failures, setting a 30-second per-test timeout and printing only failing tests.[3] The HTTP API test suite (test:httpapi in packages/opencode/package.json) runs script/httpapi-exercise.ts three times in sequence — coverage mode, auth mode, and effect mode — each with --fail-on-missing and --fail-on-skip.[3]
Sources
Updated
OpenCode contributions flow through issue templates, linked PRs, and design review gates: issues must use Bug report, Feature request, or Question templates; PRs must reference an issue and follow conventional commit format; UI and core features need core team design sign-off before implementation. The codebase enforces style consistency through Prettier (semicolon-free, 120-char lines) and code-style preferences: no else blocks, .catch() for errors, precise types, immutable patterns, and Bun APIs.
All issues must use one of three templates — Bug report, Feature request, or Question — and blank issues are not allowed.[1] All PRs must reference an existing issue using Fixes #123 or Closes #123 in the PR description; PRs without a linked issue may be closed without review.[1]
Any UI or core product feature must go through a design review with the core team before implementation; PRs that skip this step will likely be closed.[1] New provider support should be contributed first to https://github.com/anomalyco/models.dev and should not require significant code changes to the opencode core.[1]
PR titles must follow conventional commit standards with prefixes: feat:, fix:, docs:, chore:, refactor:, or test:, optionally scoped to a package (e.g., feat(app):).[1] Prettier is configured in package.json with semi: false and printWidth: 120.[2]
The project code style prefers: no else statements, .catch(...) over try/catch, precise types over any, immutable patterns over let, single-word concise identifiers, and Bun-native APIs like Bun.file().[1]
Sources
Updated
Pages in this section:
Updated
OpenCode CLI commands wire through packages/opencode/src/index.ts as a yargs parser with global flags for logging (--log-level, --print-logs) and behavior (--pure for plugin-free mode), and top-level subcommands for interactive (tui, attach), non-interactive (run, serve), and IDE-integrated (acp) modes. The TUI starts by default and supports session resume/fork, custom models/agents, and server binding; serve runs a headless HTTP API; attach connects to remote backends; and acp integrates with editors via JSON-RPC—all sharing OpenCode's tools, MCP servers, and agent permission system. MCP (Model Context Protocol) servers are external processes that expose additional tools and context to OpenCode agents.
packages/opencode/src/index.ts is the CLI entry point, wiring all subcommands into a yargs parser with --print-logs, --log-level, and --pure global flags.[1] The --log-level flag in packages/opencode/src/index.ts accepts DEBUG, INFO, WARN, or ERROR and propagates the choice via process.env.OPENCODE_LOG_LEVEL.[1] The --pure flag in packages/opencode/src/index.ts sets process.env.OPENCODE_PURE = "1" to run without external plugins.[1] The CLI middleware in packages/opencode/src/index.ts always sets process.env.AGENT = "1", process.env.OPENCODE = "1", and process.env.OPENCODE_PID to the current process PID before any command runs.[1]
packages/opencode/src/index.ts registers the following top-level CLI commands: acp, mcp, tui, attach, run, generate, debug, account, providers, agent, upgrade, uninstall, serve, web, models, stats, export, import, github, pr, session, plugin, and db.[1] The cmd helper in packages/opencode/src/cli/cmd/cmd.ts is a typed identity function that wraps a yargs CommandModule and enriches its argument type with WithDoubleDash<U>, which adds optional "--" (passthrough args) and _ (positional args) fields.[2]
The opencode CLI starts the TUI by default when run without arguments; passing a prompt via opencode run enables non-interactive (programmatic) usage.[3] The opencode run command runs OpenCode non-interactively by accepting a prompt directly, useful for scripting and automation without launching the TUI.[3] The opencode TUI command supports flags including --continue/-c (resume last session), --session/-s (resume by session ID), --fork (fork when continuing), --prompt, --model/-m (in provider/model format), --agent, --auto (auto-approve non-denied permissions), --port, --hostname, --mdns, --mdns-domain, and --cors.[3] The TUI randomly assigns a port and hostname by default; passing explicit --hostname and --port flags allows external clients to connect to the TUI's server.[4]
opencode serve runs a headless HTTP server exposing an OpenAPI endpoint that opencode clients can use programmatically, defaulting to port 4096 and hostname 127.0.0.1.[4] mDNS discovery for the opencode server is disabled by default (--mdns defaults to false); the default mDNS service domain is opencode.local (--mdns-domain).[4] The --cors flag on opencode serve allows additional browser origins and can be passed multiple times to allow several origins.[4] Set the OPENCODE_SERVER_PASSWORD environment variable to protect the opencode server with HTTP basic auth; the username defaults to opencode and can be overridden with OPENCODE_SERVER_USERNAME. This applies to both opencode serve and opencode web.[4]
Example: Enable HTTP basic auth on the opencode server by setting OPENCODE_SERVER_PASSWORD:
OPENCODE_SERVER_PASSWORD=your-password opencode serve
opencode attach [url] attaches a TUI to an already-running OpenCode backend server (started via serve or web commands), enabling TUI use with a remote backend.[3] Using opencode attach with a separately started opencode serve avoids MCP server cold boot times when running opencode run repeatedly.[3] The --password flag for opencode attach defaults to the OPENCODE_SERVER_PASSWORD environment variable; --username defaults to OPENCODE_SERVER_USERNAME or opencode.[3] Example: attaching the TUI to a remote OpenCode backend started with opencode web.[3]
opencode acp starts OpenCode as an ACP-compatible subprocess that communicates with a host editor over JSON-RPC via stdio.[5] When using OpenCode via ACP, all features work identically to the terminal — including built-in tools, custom tools and slash commands, MCP servers, AGENTS.md rules, and the agents/permissions system — except some built-in slash commands (/undo, /redo) which are currently unsupported.[5]
Example: Configure Zed to use a custom OpenCode executable via ACP by adding to ~/.config/zed/settings.json:
{
"agent_servers": {
"OpenCode": {
"type": "custom",
"command": "opencode",
"args": ["acp"]
}
}
}
Example: Configure OpenCode as an ACP agent in JetBrains IDEs via acp.json:
{
"agent_servers": {
"OpenCode": {
"command": "/absolute/path/bin/opencode",
"args": ["acp"]
}
}
}
ACP (Agent Communication Protocol) is a JSON-RPC–based protocol enabling host editors (such as Zed or JetBrains IDEs) to communicate with an external AI agent subprocess over stdio.
Running /init inside the OpenCode TUI analyzes the project and creates an AGENTS.md file in the project root; this file should be committed to Git.[6] The /share command creates a shareable link to the current conversation and copies it to the clipboard; conversations are not shared by default.[6] The /redo command re-applies changes that were previously undone with /undo.[6] The @ key in the TUI opens a fuzzy file search to include files in the prompt.[6] Images can be added to an OpenCode prompt by dragging and dropping them into the terminal.[6]
On unknown arguments, missing required arguments, or invalid values, packages/opencode/src/index.ts calls cli.showHelp() rather than printing a bare error message.[1] When --help / -h is passed, packages/opencode/src/index.ts prepends the UI.logo() banner to help output when the text does not already start with "opencode ".[1] On a caught error, packages/opencode/src/index.ts calls FormatError(e) and, if the result is undefined (unrecognized error), falls back to printing "Unexpected error" plus the raw error message, then sets process.exitCode = 1.[1] packages/opencode/src/index.ts always calls process.exit() in the finally block after command completion, forcefully terminating any subprocesses (such as Docker-based MCP servers) that do not handle SIGTERM.[1]
Sources
Updated
OpenCode exposes an OpenAPI 3.1-compliant HTTP API where the /session path group provides CRUD and action endpoints for managing sessions (create, list, fork, message, etc.), while /global and other path groups expose health checks, events, and ancillary services. The server is built on Effect as a composable web handler (OpenCodeHttpApi), shipped as both a lazily-initialized singleton (Server.Default) and a configurable listen function that returns a plain Listener object for stopping/force-closing connections.
The opencode server exposes an OpenAPI 3.1 spec at http://<hostname>:<port>/doc (e.g., http://localhost:4096/doc), which can be used to generate clients or viewed in a Swagger explorer.[1] Server.openapi() in packages/opencode/src/server/server.ts generates and returns the OpenAPI schema from PublicApi.[2] Server.Default in packages/opencode/src/server/server.ts is a lazily-initialized singleton that provides a fetch/request handler backed by HttpApiApp.webHandler(), suitable for in-process request dispatch without a real TCP listener.[2]
The OpenCodeHttpApi in packages/opencode/src/server/routes/instance/httpapi/api.ts is the root HTTP API that composes RootHttpApi, EventApi, InstanceHttpApi, ServerApi, and PtyConnectApi into a single Effect HttpApi.[3] RootHttpApi in packages/opencode/src/server/routes/instance/httpapi/api.ts applies SchemaErrorMiddleware and Authorization middleware to the ControlApi, ControlPlaneApi, and GlobalApi route groups.[3] OpenCodeHttpApi in packages/opencode/src/server/routes/instance/httpapi/api.ts registers additional schemas (EventSchema, Question.Replied, Question.Rejected, Credential.Value, Integration.Inputs, Integration.Method, Integration.Ref, SkillV2.Source) via HttpApi.AdditionalSchemas for OpenAPI generation.[3] The EventSchema in packages/opencode/src/server/routes/instance/httpapi/api.ts is a Schema.Union of all latest EventManifest event types plus InstanceDisposed, annotated with the identifier "Event".[3]
The Global API includes GET /global/health (returns { healthy: true, version: string }) and GET /global/event (an SSE stream of global events).[1] The /tui endpoint can drive the TUI programmatically — for example, to prefill or run a prompt — and this mechanism is used by OpenCode IDE plugins.[1]
packages/opencode/src/server/routes/instance/httpapi/groups/session.ts defines all session HTTP API endpoints under the /session path prefix, including: GET /session (list), GET /session/status, GET /session/:sessionID, GET /session/:sessionID/children, GET /session/:sessionID/todo, GET /session/:sessionID/diff, GET /session/:sessionID/message, GET /session/:sessionID/message/:messageID, POST /session (create), DELETE /session/:sessionID, PATCH /session/:sessionID (update), POST /session/:sessionID/fork, POST /session/:sessionID/abort, POST /session/:sessionID/share, POST /session/:sessionID/init, POST /session/:sessionID/summarize, POST /session/:sessionID/message (prompt), POST /session/:sessionID/prompt_async, POST /session/:sessionID/command, POST /session/:sessionID/shell, POST /session/:sessionID/revert, POST /session/:sessionID/unrevert, PATCH/DELETE /session/:sessionID/message/:messageID, and PATCH/DELETE /session/:sessionID/message/:messageID/part/:partID.[4] All session API endpoints in packages/opencode/src/server/routes/instance/httpapi/groups/session.ts require workspace routing query fields (via WorkspaceRoutingQuery or WorkspaceRoutingQueryFields) for multi-workspace targeting.[4]
The GET /session list endpoint in packages/opencode/src/server/routes/instance/httpapi/groups/session.ts accepts query params: scope (optional, "project"), path (optional string), roots (optional boolean), start (optional number), search (optional string), and limit (optional number), and returns sessions sorted by most recently updated.[4] The GET /session/:sessionID/message endpoint in packages/opencode/src/server/routes/instance/httpapi/groups/session.ts supports pagination via a limit (non-negative integer) and before (string cursor) query parameter, returning messages as SessionV1.WithParts[].[4] The GET /session/:sessionID/diff endpoint in packages/opencode/src/server/routes/instance/httpapi/groups/session.ts returns Snapshot.FileDiff[] representing file changes that resulted from a specific user message in the session.[4] The POST /session/:sessionID/message endpoint sends a message and waits for a response; its body accepts { messageID?, model?, agent?, noReply?, system?, tools?, parts }.[1] The PATCH /session/:sessionID update endpoint in packages/opencode/src/server/routes/instance/httpapi/groups/session.ts accepts optional fields: title (string), metadata (Session.Metadata), permission (PermissionV1.Ruleset), and time.archived (Session.ArchivedTimestamp).[4] DELETE /session/:sessionID in packages/opencode/src/server/routes/instance/httpapi/groups/session.ts permanently removes a session and all associated data including messages and history.[4] The POST /session/:sessionID/fork endpoint in packages/opencode/src/server/routes/instance/httpapi/groups/session.ts creates a new session by forking an existing session at a specific message point; its payload is derived from Session.ForkInput minus the sessionID (provided via path param).[4] The POST /session/:sessionID/init endpoint in packages/opencode/src/server/routes/instance/httpapi/groups/session.ts analyzes the app and creates AGENTS.md; its payload requires modelID, providerID, and messageID.[4][1] The POST /session/:sessionID/summarize endpoint in packages/opencode/src/server/routes/instance/httpapi/groups/session.ts accepts providerID, modelID, and an optional auto boolean flag.[4] The POST /session/:sessionID/permissions/:permissionID endpoint in packages/opencode/src/server/routes/instance/httpapi/groups/session.ts accepts a response field typed as PermissionV1.Reply, allowing the client to answer a pending permission request.[4]
The Server.listen function in packages/opencode/src/server/server.ts wraps the Effect-based listenEffect and returns a plain Listener object (with hostname, port, url, and stop) that callers outside the Effect runtime can use.[2] NodeHttpServer is configured with a gracefulShutdownTimeout of "1 second" in packages/opencode/src/server/server.ts.[2] listener.stop(true) in packages/opencode/src/server/server.ts force-closes all active HTTP connections and WebSocket connections concurrently before closing the listener scope; stop(false) or stop() performs a graceful shutdown without forcing connections.[2] forceClose in packages/opencode/src/server/server.ts calls both state.http.closeAll and state.websockets.closeAll concurrently with unbounded concurrency.[2] The serverLayer function in packages/opencode/src/server/server.ts monkey-patches server.close so that when forceStop is set (via ListenerServerService.closeAll), server.closeAllConnections() is called immediately upon server.close() invocation, allowing NodeHttpServer's own shutdown finalizer to honour the forced-close flag.[2] The mDNS unpublish effect in packages/opencode/src/server/server.ts is registered as a scope finalizer so it runs automatically when the listener's scope is closed.[2]
packages/opencode/src/server/server.ts suppresses AI SDK stdout warnings globally by setting globalThis.AI_SDK_LOG_WARNINGS = false before the server starts.[2]
Sources
Updated
Pages in this section:
Updated
OpenCode supports both JSON and JSONC (JSON with Comments) formats for its config file, referenced by the $schema key https://opencode.ai/config.json.[1] Configuration files from all sources are merged together, not replaced: non-conflicting keys from all sources are preserved, and later sources override only conflicting keys.[1] Config sources are loaded in this precedence order (lowest to highest): Remote config (.well-known/opencode), Global config (~/.config/opencode/opencode.json), Custom config (OPENCODE_CONFIG env var), Project config (opencode.json in project root), .opencode directories, Inline config (OPENCODE_CONFIG_CONTENT env var), Managed file config (e.g., /Library/Application Support/opencode/ on macOS), and macOS MDM managed preferences (ai.opencode.managed).[1]
The global OpenCode config lives at ~/.config/opencode/opencode.json; TUI-specific global settings use ~/.config/opencode/tui.json.[1] When OpenCode starts, it looks for a project config (opencode.json) first in the current directory, then traverses up to the nearest Git directory.[1] A custom config file path can be set with the OPENCODE_CONFIG environment variable; it is loaded between global and project configs in precedence order.[1] The .opencode and ~/.config/opencode directories use plural names for subdirectories (agents/, commands/, modes/, plugins/, skills/, tools/, themes/); singular names are also supported for backwards compatibility.[1]
Remote config is fetched automatically from the .well-known/opencode endpoint when authenticating with a supporting provider, and serves as the base config layer that all other sources can override.[1] Organizations can enforce configuration via managed config files dropped in platform-specific system directories: /Library/Application Support/opencode/ (macOS), /etc/opencode/ (Linux), or %ProgramData%\opencode (Windows); these directories require admin/root access to write.[1] The resolved configuration (including managed preferences) can be inspected by running opencode debug config.[1]
The global config file is resolved by checking candidates opencode.jsonc, opencode.json, and config.json (in that order) under Global.Path.config; if none exist, opencode.jsonc is used as the write target.[2] On first load of a global config file that lacks $schema, config.ts automatically injects "$schema": "https://opencode.ai/config.json" and rewrites the file to enable editor completion.[2] The $schema auto-injection is skipped when any of the Flag.OPENCODE_CONFIG, Flag.OPENCODE_CONFIG_DIR, or Flag.OPENCODE_CONFIG_CONTENT flags are set, preventing file writes when config is provided via environment.[2]
loadConfig in config.ts runs ConfigVariable.substitute on raw config text before JSON parsing, expanding well-known variable placeholders in place.[2] loadConfig calls ConfigV2Compat.lower on the raw parsed config before decoding with ConfigV1.Info, providing backward compatibility with v2 config fields in the v1 schema path.[2] Legacy config keys theme, keybinds, and tui are silently stripped from any loaded config object in normalizeLoadedConfig to prevent stale v1 UI config from affecting v2 behavior; the documentation notes these keys are deprecated and automatically migrated when possible.[2][1] When merging config objects, mergeConfigConcatArrays in config.ts concatenates (and deduplicates) the instructions array instead of replacing it, so instructions from multiple config layers accumulate.[2] Remote config URLs are fetched with withTransientReadRetry; if the response Content-Type is HTML or the body matches <!doctype or <html, the fetch is treated as an auth-proxy login redirect and terminates with a RemoteAuthError rather than a decode failure.[2]
plugin_origins is a derived runtime field on Config.Info (not persisted) that tracks, for each winning plugin spec, the file and scope it came from so location-sensitive decisions can be made at runtime.[2] The writable helper in config.ts strips plugin_origins before persisting config, ensuring that derived runtime state is never written back to disk.[2] The writableGlobal helper additionally removes the shell key when its value is an empty string, preventing a blank "shell": "" entry from being written to the global config file when the user resets a value to default in the Desktop app.[2] Config JSON/JSONC patching is performed by patchJsonc in config.ts, which recursively applies jsonc-parser's modify+applyEdits with 2-space indentation rather than a full round-trip serialization, preserving comments and formatting.[2]
The shell used by OpenCode for the interactive terminal and agent tool calls is set via the shell config key; if not specified, OpenCode auto-discovers a sensible default (pwsh or cmd.exe on Windows, /bin/zsh or /bin/bash on macOS/Linux).[1]
When cursor.style is "default" in tui.json, the terminal default cursor is restored and cursor.blinking has no effect.[1]
The Config service (packages/opencode/src/config/config.ts) is composed from sub-modules ConfigAgent, ConfigCommand, ConfigManaged, ConfigParse, ConfigPaths, ConfigPlugin, ConfigVariable, and ConfigV2Compat.[2] The Config.Service class is registered under the Effect context tag "@opencode/Config" and exports a use helper via serviceUse.[2] The Config.Interface exposes get, getGlobal, getConsoleState, update, updateGlobal, invalidate, directories, and waitForDependencies as the public API of the config service.[2] config.ts depends on Auth.Service, Account.Service, Env.Service, Npm.Service, FSUtil.Service, and HttpClient.HttpClient as Effect service dependencies required to construct the Config layer.[2] config.ts notes (in a comment) that remeda's mergeDeep conditional-merge generic dominates TypeScript profiling in hot config-loading paths; the mergeConfig wrapper uses a local cast to keep the expensive type out of critical paths.[2]
The ConfigAgent.load function scans {agent,agents}/**/*.md under a given directory (with dot and symlink support) and parses each file as a markdown-with-frontmatter agent config, deriving the agent name via configEntryNameFromPath stripping agent/ or agents/ prefixes.[3] Each agent config entry has its prompt field set to the trimmed markdown body (md.content.trim()), while frontmatter fields are spread into the config object alongside the derived name.[3] ConfigAgent.load calls ConfigParse.schema(ConfigAgentV1.Info, config, item) to validate and decode each agent entry against the ConfigAgentV1.Info schema, with the file path passed for error reporting.[3] Agent config files that fail markdown parsing (throw during ConfigMarkdown.parse) are silently skipped — the .catch(() => undefined) guard prevents one bad file from aborting the entire load.[3] Mode configs loaded by ConfigAgent.loadMode always have mode: "primary" forced onto the decoded value, regardless of what the frontmatter specifies.[3] ConfigAgent.loadMode uses Schema.decodeUnknownExit with { errors: "all", propertyOrder: "original" } for validation, while ConfigAgent.load delegates to ConfigParse.schema — the two loaders use different validation strategies.[3]
Configure plugins with package strings or { package, options } objects; skills as a discovery-source array; references as a keyed alias map; formatter and lsp as boolean or override records; and attachments/tool_output with limit objects.
{
"plugins": [
"opencode-helicone-session",
{ "package": "@my-org/audit-plugin", "options": { "endpoint": "https://audit.example.com" } }
],
"skills": ["./team-skills", "~/shared-skills", "https://example.com/.well-known/skills/"],
"references": {
"design-system": { "path": "../ui-library" },
"sdk": { "repository": "github.com/example/sdk", "branch": "main" }
},
"formatter": {
"prettier": { "disabled": true },
"project": { "command": ["./scripts/format", "$FILE"], "extensions": [".foo"] }
},
"lsp": {
"typescript": { "disabled": true },
"project": { "command": ["project-language-server", "--stdio"], "extensions": [".foo"] }
},
"attachments": { "image": { "auto_resize": true, "max_width": 2000, "max_height": 2000 } },
"tool_output": { "max_lines": 2000, "max_bytes": 51200 }
}
Sources
Updated
MCP servers are defined in opencode.jsonc under the mcp key, where each server is identified by a unique name used to reference it in prompts.[1] Once added, MCP tools are automatically available to the LLM alongside built-in tools without any additional wiring.[1] Each MCP server adds tokens to the LLM's context window; servers with large tool catalogs (e.g., the GitHub MCP server) can easily exceed the context limit.[1] MCP (Model Context Protocol) is an open standard that lets external tools and services expose callable functions to an LLM through a structured interface; each MCP server hosts one or more such tools the LLM can invoke during a conversation.
opencode mcp add interactively guides adding a local or remote MCP server to the configuration; opencode mcp list shows all configured servers and their connection status.[2] opencode mcp auth <server-name> manually triggers the OAuth browser flow for a specific MCP server; opencode mcp logout <server-name> removes stored credentials.[1] opencode mcp debug <server-name> shows auth status, tests HTTP connectivity, and attempts the OAuth discovery flow to diagnose connection issues.[1]
Local MCP servers require type: "local" and a command array (e.g., ["npx", "-y", "my-mcp-command"]); the cwd, environment, enabled, and timeout fields are optional.[1] For local MCP servers, relative paths in the cwd field resolve from the workspace root.[1] Example: adding the @modelcontextprotocol/server-everything local MCP server and referencing it in a prompt by name.[1]
Remote MCP servers require type: "remote" and a url; optional fields include enabled, headers, oauth, and timeout.[1] The timeout option for both local and remote MCP servers specifies the maximum time in milliseconds to wait when fetching tools, defaulting to 5000 (5 seconds).[1] An MCP server can be temporarily disabled without removing it from the config by setting its enabled field to false.[1] Organizations can publish default MCP server configurations via a .well-known/opencode endpoint; users can override these remote defaults by adding matching entries with enabled: true to their local config.[1]
opencode automatically handles OAuth for remote MCP servers: it detects a 401 response, initiates the OAuth flow using Dynamic Client Registration (RFC 7591) if supported, and stores tokens for future requests.[1] OAuth tokens are stored at ~/.local/share/opencode/mcp-auth.json after completing the browser-based authorization flow.[1] Pre-registered OAuth credentials (clientId, clientSecret, scope) can be supplied via the oauth object; if clientId is omitted, dynamic client registration is attempted automatically.[1] Setting oauth: false on a remote MCP server disables automatic OAuth detection, which is useful for servers that authenticate via API keys in headers instead.[1]
When connecting a remote MCP server, packages/opencode/src/mcp/index.ts attempts StreamableHTTP transport first and falls back to SSE transport, both using the same optional authProvider and headers.[3] On transport connection failure, packages/opencode/src/mcp/index.ts closes the transport via t.close() and ignores any close errors; on success the caller owns the transport and it is not closed.[3] A remote MCP entry with an unparseable URL immediately returns a failed status with the message Invalid MCP URL for "<name>" rather than throwing.[3] Pending OAuth transports are stored in a module-level Map (pendingOAuthTransports) in packages/opencode/src/mcp/index.ts, keyed by server name, so that the finishAuth flow can retrieve and reuse the already-negotiated transport.[3]
Each MCP client is created with roots capability enabled, while sampling, elicitation, and tasks capabilities are explicitly commented out in packages/opencode/src/mcp/index.ts, with issue-tracker links for each pending capability.[3] packages/opencode/src/mcp/index.ts registers a ListRootsRequestSchema handler that responds with the current project directory as a file URL, so MCP servers can discover the workspace root.[3]
packages/opencode/src/mcp/index.ts exports an MCPStatus union (Status) with five discriminated variants: connected, disabled, failed, needs_auth, and needs_client_registration.[3] McpTool in packages/opencode/src/mcp/index.ts holds a shared cached def (the raw MCP tool definition) and the owning client; the doc comment explicitly warns consumers to copy rather than mutate the def.[3] packages/opencode/src/mcp/index.ts wires the MCP Service layer by acquiring ChildProcessSpawner, McpAuth.Service, EventV2Bridge.Service, and McpBrowser.Service as dependencies.[3]
Sources
Updated
A plugin is a JavaScript or TypeScript module that hooks into OpenCode's lifecycle and event streams by exporting an async function that receives a context object (project, directory, worktree, client, shell) and returns hooks keyed by event name, allowing code to intercept commands, events, and LLM-driven tool execution. Plugins load from npm packages, local directories, or global config; hooks run sequentially in registration order (global config, project config, global plugins, local plugins), and plugins can define custom tools, override built-in tools, register workspace adapters, or modify LLM requests — all governed by a lifecycle (config initialization, event streaming, dispose cleanup) and optional auth/provider customization.
The plugin subsystem lives in packages/opencode/src/plugin/index.ts and exposes a Service Effect context tag keyed @opencode/Plugin, implementing trigger, list, and init on the Interface type.[1] A plugin is a JavaScript or TypeScript module that exports an async function receiving a context object (project, directory, worktree, client, $) and returning a hooks object keyed by event name.[2] TypeScript plugins can import the Plugin type from @opencode-ai/plugin for type-safe hook implementations; in packages/plugin/src/index.ts, Plugin is typed as (input: PluginInput, options?: PluginOptions) => Promise<Hooks>, and a PluginModule wraps it as { id?: string; server: Plugin; tui?: never }.[2][3] The GitHub Copilot plugin in packages/opencode/src/plugin/github-copilot/copilot.ts attaches an X-Interaction-Id HTTP header — populated with the current session ID — to all outgoing Copilot API calls, enabling GitHub's API to attribute requests to their corresponding session for audit and telemetry.
PluginInput in packages/plugin/src/index.ts provides plugins with an @opencode-ai/sdk client, the current project, directory, worktree, a serverUrl URL, access to Bun's shell ($), and an experimental_workspace.register method to register custom WorkspaceAdapter implementations.[3] At runtime, packages/opencode/src/plugin/index.ts constructs the PluginInput client with a createOpencodeClient instance pointed at the running server URL, falling back to http://localhost:4096 and direct Server.Default().app.fetch when no server URL is available.[1] The WorkspaceAdapter interface in packages/plugin/src/index.ts requires implementations to supply a name, description, configure, create, remove, and target method, where target returns either a local directory path or a remote URL with optional headers.[3] A WorkspaceAdapter registered via a plugin allows OpenCode to redirect file and shell operations to non-local environments (such as remote containers or VMs); without a registered adapter, OpenCode operates only on the local filesystem.
Plugins can be loaded from local files placed in .opencode/plugins/ (project-level) or ~/.config/opencode/plugins/ (global); files in these directories are loaded automatically at startup.[2] npm plugins are specified in the plugin config key as an array of package names and are automatically installed using Bun at startup; packages and their dependencies are cached in ~/.cache/opencode/node_modules/.[2] Local plugins that need external npm packages must add a package.json to the config directory (e.g., .opencode/package.json) with the required dependencies; opencode runs bun install at startup to install them.[2] Plugin load order is: global config (~/.config/opencode/opencode.json), project config (opencode.json), global plugin directory (~/.config/opencode/plugins/), project plugin directory (.opencode/plugins/); all hooks from all sources run in sequence.[2] Duplicate npm plugins with the same name and version are loaded once, but a local plugin and an npm plugin with similar names are both loaded separately.[2]
packages/opencode/src/plugin/index.ts uses PluginLoader.loadExternal to install and load user-configured plugins listed in cfg.plugin_origins; the pure runtime flag suppresses all external plugins, and plugins are waited on only after config.waitForDependencies() resolves.[1] The disableDefaultPlugins runtime flag in packages/opencode/src/plugin/index.ts suppresses all internal (built-in) plugins; when set, internalPlugins(flags) is replaced with an empty array.[1] External plugin execution in packages/opencode/src/plugin/index.ts is kept sequential (Effect.tryPromise one at a time in a for-loop) so that hook registration and execution order is deterministic.[1] After all hooks are registered, packages/opencode/src/plugin/index.ts calls each hook's config method with the current config object; errors in config hooks are logged but not fatal (Effect.ignore).[1] packages/opencode/src/plugin/index.ts subscribes to the EventV2Bridge event stream and fans out every event to all registered hooks' event method, filtered to events whose location.directory matches the current workspace directory.[1] On finalization (scope close), packages/opencode/src/plugin/index.ts calls each hook's dispose method sequentially; errors are logged but do not abort disposal of subsequent hooks (Effect.ignore).[1] The experimentalWebSocketsEnabled helper in packages/opencode/src/plugin/index.ts returns true if the enabled flag is set OR the installation channel is one of local, dev, or beta, meaning pre-release builds enable experimental WebSockets by default without an explicit opt-in.[1]
V1 plugin modules are detected by readV1Plugin in packages/opencode/src/plugin/index.ts; if a V1 plugin is found, its server export is called and the returned Hooks object pushed onto the hooks list. If no V1 plugin is found, the module falls back to getLegacyPlugins, which scans all named exports for callable server plugins.[1] The getLegacyPlugins function in packages/opencode/src/plugin/index.ts throws TypeError: Plugin export is not a function if any named export from a legacy plugin module is not a callable server plugin.[1]
The TriggerName type in packages/opencode/src/plugin/index.ts constrains triggerable hook names to only those whose signatures match (input: any, output: any) => Promise<void>, excluding lifecycle hooks like dispose, event, and config.[1] The full list of plugin events includes: command.executed, file.edited, file.watcher.updated, installation.updated, lsp.client.diagnostics, lsp.updated, message.part.removed, message.part.updated, message.removed, message.updated, permission.asked, permission.replied, server.connected, session.created, session.compacted, session.deleted, session.diff, session.error, session.idle, session.status, session.updated, todo.updated, shell.env, tool.execute.after, tool.execute.before, tui.prompt.append, tui.command.execute, and tui.toast.show.[2] The tool.definition hook in packages/plugin/src/index.ts allows plugins to modify the description and parameters of a tool definition sent to the LLM, identified by toolID.[3] The experimental.session.compacting hook in packages/plugin/src/index.ts is called before session compaction starts and allows plugins to append extra context strings or entirely replace the compaction prompt.[3] The experimental.compaction.autocontinue hook in packages/plugin/src/index.ts is called after compaction and before the synthetic auto-continue message is added; setting output.enabled to false suppresses the synthetic user "continue" turn.[3] The AuthHook type in packages/plugin/src/index.ts supports two auth method types: oauth (which calls authorize and returns a URL + callback) and api (which prompts for keys and optionally calls authorize). Both method types support text or select prompt steps with a when Rule condition; the older condition callback is deprecated in favor of when.[3] The ProviderHook type in packages/plugin/src/index.ts requires a string id and an optional models callback that receives a V2 Provider and auth context and returns a record of V2 Model objects, allowing plugins to supply custom model lists.[3] The AuthOuathResult type alias in packages/plugin/src/index.ts is deprecated; AuthOAuthResult (corrected spelling) should be used instead.[3]
Example: a tool.execute.before hook that blocks reading .env files by throwing an error when the read tool targets a path containing .env.[2] Example: a shell.env hook that injects environment variables into all shell executions (both AI tool calls and user terminals).[2]
Plugins can define custom tools using the tool helper from @opencode-ai/plugin; if a plugin tool has the same name as a built-in tool, the plugin tool takes precedence.[2] Custom tools defined in plugins must provide a description, a Zod-schema args definition using tool.schema.* helpers, and an async execute function; the execute function receives args and a context object with agent, sessionID, messageID, directory, and worktree.[2][4] Use context.directory for the current session working directory and context.worktree for the git worktree root inside a custom tool's execute function.[4]
Custom tools defined as TypeScript or JavaScript files (the definition itself must be TS/JS, but can invoke scripts written in any language) are placed in .opencode/tools/ for project-local scope or ~/.config/opencode/tools/ for global scope — see Tools for plugin-system internals.[4] The tool() helper from @opencode-ai/plugin provides type-safety and validation when defining standalone custom tools; tool.schema is Zod, but argument schemas can also be defined by importing Zod directly and returning a plain object without the helper.[4] The filename of a custom tool file becomes the tool name: a default export from database.ts creates a database tool, while named exports add and multiply from math.ts create math_add and math_multiply.[4] If a custom tool shares the same name as a built-in tool, the custom tool takes precedence and replaces the built-in; to disable a built-in without replacing it, use the permissions system instead.[4] Custom tools can invoke scripts in any language by using Bun.$ to shell out; a TypeScript wrapper calls the external script and returns its output.[4]
Canonical pattern for calling a Python script from a custom tool using Bun.$ and context.worktree:
async execute(args, context) {
const script = path.join(context.worktree, ".opencode/tools/add.py")
const result = await Bun.$`python3 ${script} ${args.a} ${args.b}`.text()
return result.trim()
}
The V2 Promise Plugin API (@opencode-ai/plugin/v2/promise) provides the same hook and reload capabilities as the Effect API (@opencode-ai/plugin/v2/effect) but uses Promises instead of Effects for all async boundaries.[5] V2 Promise plugins are defined with define({ id, setup }) from @opencode-ai/plugin/v2/promise; the setup function receives a ctx object and registers hooks imperatively — it does not return a hook object, and per-plugin options are available as ctx.options.[5] V2 Promise transform hooks are available on six domain namespaces in ctx: agent, catalog, command, integration, reference, and skill; each supports a .transform() call and a .reload() call.[5] A V2 Promise transform registration can be removed early by calling registration.dispose(); the returned Registration object holds an async dispose method.[5] To refresh a V2 plugin domain after external data changes, call ctx.<domain>.reload() after updating the data; reload re-executes all registered transform hooks for that domain.[5] The V2 Promise runtime hooks ctx.aisdk.sdk and ctx.aisdk.language allow intercepting AI SDK module loading and language model resolution respectively, enabling plugins to inject custom provider SDK instances.[5]
Canonical V2 Promise plugin definition using define from @opencode-ai/plugin/v2/promise:
import { define } from "@opencode-ai/plugin/v2/promise"
export const Plugin = define({
id: "example",
setup: async (ctx) => {
await ctx.catalog.transform((catalog) => {
catalog.provider.update("example", (provider) => {
provider.name = "Example"
})
})
},
})
The V2 plugin system exposes two entry points: @opencode-ai/plugin/v2/effect (Effect-based) and @opencode-ai/plugin/v2/promise (Promise-based); the Promise API is recommended for codebases that do not already use the Effect library.
Sources
Updated
Upgrading documents version-specific changes in OpenCode Desktop, TUI, SDKs, and provider integrations; each release notes new features, fixes, and compatibility adjustments across layout migration, session features, model routing, and third-party service support. Mixed-version setups and legacy config handling require careful attention: newer OpenCode versions maintain backward compatibility with older configs while removing reliance on legacy configuration on v2 servers.
Desktop v1.18.0 completed the Desktop v2 migration, including upgrade handling for the new layout and first-launch onboarding, with a setting to switch between the new and old layouts during the transition period.[1] Desktop v1.18.12 skips legacy config reads against v2 servers to avoid spurious config loading issues; upgraders running mixed v1/v2 setups should be aware that v2 servers no longer consult legacy config.[2]
As of v1.18.24, V1 reads supported V2 config fields so newer config files remain functional in mixed V1/V2 setups.[3] Unknown top-level config fields are ignored instead of failing config parsing, as of v1.18.16.[4]
Session snapshots and revert controls allow rolling a session back to an earlier message, including reverting file changes, added in v1.17.11.[5] Chronological message ordering stays correct even when imported or legacy message IDs are out of order, fixed in v1.18.15.[6] Revert and fork actions use real message chronology instead of message ID ordering, as of v1.18.15.[6] Full session transcripts can be exported as JSON from the Desktop UI, added in v1.18.15.[6] Session compaction keeps complete recent turns and produces clearer summaries for smaller models, improved in v1.18.17.[7]
Subagents no longer launch nested subagents by default; a configurable subagent_depth limit is available when nesting is needed, as of v1.18.2.[8] A yolo mode to auto-approve permissions was added to the TUI in v1.17.12.[9]
Azure providers can sign in with Microsoft Entra ID through the Azure CLI instead of requiring an API key, as of v1.18.24.[3] Azure Cognitive Services endpoint support for Azure-hosted models was restored in v1.18.4.[10] Azure AI support for GPT-5.6 was added in v1.17.20.[11] Bedrock reasoning responses are no longer cached into unreplayable empty messages, fixed in v1.18.24.[3] Anthropic model IDs with dots (e.g. claude-haiku-4.5) are converted to the dashed slug Anthropic expects when routing through Cloudflare AI Gateway, as of v1.18.23.[12] Non-Workers (third-party) models routed through Cloudflare AI Gateway use the gateway's REST API, fixed in v1.18.23.[12] Native OpenAI and Anthropic passthroughs for Cloudflare AI Gateway models were added in v1.18.19.[13] textVerbosity is no longer sent to OpenAI-compatible providers that do not support it, fixed in v1.18.22.[14] Vertex AI eu and us multi-region Gemini requests are routed through REP endpoints, as of v1.18.21.[15] Deprecated sampling defaults are no longer sent to newer Gemini models, as of v1.18.8.[16] Kimi family models on Anthropic-compatible providers use adaptive thinking controls with summarized reasoning output by default, as of v1.18.4.[10] Reasoning mode is forced for OpenAI-compatible reasoning models so reasoning settings apply reliably on custom deployments, as of v1.17.13.[17] An obsolete Codex workaround that could interfere with OpenAI Luna Responses Lite requests was removed in v1.17.20.[11] Response storage is disabled by default for xAI Responses models, as of v1.17.19.[18] xAI login was simplified to a single device-code flow that works in headless and remote environments, as of v1.18.14.[19] Cerebras max_completion_tokens is preserved without applying an extra output cap, as of v1.18.20.[20] The provider layer retries responses ending with finish_reason: network_error and also handles the variants network-error and network_error, as of v1.18.20.[20] Available Modal models are discovered automatically, as of v1.18.10.[21]
MCP servers reconnect after expired OAuth sessions, including during concurrent requests, and honor configured OAuth callback ports in mcp debug, as of v1.18.8.[16] Compatibility with legacy MCP SDK clients was restored in v1.18.9.[22] Paginated MCP tool catalogs no longer lose tool metadata and output schema validation, fixed in v1.17.14.[23] A code mode MCP adapter for running confined orchestration scripts against connected MCP tools was added in v1.17.14; the execute tool is hidden unless code mode is enabled.[23]
The SDK gained live event subscription streams, active session access, paged durable session history, and session permission request endpoints in v1.17.12.[9] GitHub Copilot model routing honors each model's advertised chat or responses endpoint, fixed in v1.17.14.[23] The host directory path is no longer forwarded to remote workspaces, so prompts resolve from the remote project root instead of the local host path, fixed in v1.18.14.[19] Per-prompt model selection in the composer was added to the Desktop in v1.17.19.[18]
Sources
github.com/anomalyco/opencode/releases/tag/v1.18.0github.com…anomalyco/opencode/releases/tag/v1.18.12github.com…anomalyco/opencode/releases/tag/v1.18.24github.com…anomalyco/opencode/releases/tag/v1.18.16github.com…anomalyco/opencode/releases/tag/v1.17.11github.com…anomalyco/opencode/releases/tag/v1.18.15github.com…anomalyco/opencode/releases/tag/v1.18.17github.com/anomalyco/opencode/releases/tag/v1.18.2github.com…anomalyco/opencode/releases/tag/v1.17.12github.com/anomalyco/opencode/releases/tag/v1.18.4github.com…anomalyco/opencode/releases/tag/v1.17.20github.com…anomalyco/opencode/releases/tag/v1.18.23github.com…anomalyco/opencode/releases/tag/v1.18.19github.com…anomalyco/opencode/releases/tag/v1.18.22github.com…anomalyco/opencode/releases/tag/v1.18.21github.com/anomalyco/opencode/releases/tag/v1.18.8github.com…anomalyco/opencode/releases/tag/v1.17.13github.com…anomalyco/opencode/releases/tag/v1.17.19github.com…anomalyco/opencode/releases/tag/v1.18.14github.com…anomalyco/opencode/releases/tag/v1.18.20github.com…anomalyco/opencode/releases/tag/v1.18.10github.com/anomalyco/opencode/releases/tag/v1.18.9github.com…anomalyco/opencode/releases/tag/v1.17.14Updated
Pages in this section:
Updated
OpenCode uses primary agents (Build and Plan) for direct interaction, cycling via Tab, and subagents (General, Explore, Scout) invoked by mention to delegate specialized tasks—each with distinct tool permissions and capabilities. Skills are reusable agent instructions stored in SKILL.md files, discovered across project and global locations, with access controlled by permission patterns and available on-demand to any agent via the skill tool.
OpenCode has two types of agents: primary agents, interacted with directly and cycled via the Tab key (or the configured switch_agent keybind), and subagents, invoked by primary agents or manually via @ mention.[1] OpenCode ships two built-in primary agents — Build and Plan — and three built-in subagents — General, Explore, and Scout.[1]
The Build agent is the default primary agent with all tools enabled, intended for standard development work requiring full file and system access.[1] The Plan agent is a restricted primary agent where file edits and bash permissions default to ask, preventing unintended code changes during analysis or planning.[1] Three hidden system primary agents run automatically and are not selectable in the UI: Compaction (compacts long context into a smaller summary), Title (generates short session titles, with temperature: 0.5), and Summary (generates session summaries).[1]
The General subagent has full tool access (except todowrite, which is denied to prevent it from managing the todo list) and is designed for multi-step tasks or parallel units of work; invoke it manually with @general in a message.[1][2] The Explore subagent is read-only and cannot modify files; it is optimized for fast codebase exploration using pattern and keyword searches.[1] The Scout subagent is read-only and designed for external dependency research — cloning repos into OpenCode's managed cache and cross-referencing against upstream implementations without touching the workspace.[1]
Subagents can be manually invoked by @ mentioning them in a message (e.g. @general help me search for this function); primary agents also invoke subagents automatically.[1] When subagents create child sessions, use session_child_first (default <Leader>+Down) to enter the first child session from the parent; session_child_cycle (default Right) and session_child_cycle_reverse (default Left) cycle between children; session_parent (default Up) returns to the parent.[1] Subagents run in isolated child sessions, keeping their working detail separate from the parent session's context; the parent agent receives only a summary of the subagent's result.
Agents can be configured in opencode.json under the agent key, or via markdown files placed in ~/.config/opencode/agents/ (global) or .opencode/agents/ (per-project); the markdown filename becomes the agent name.[1] Agent configuration supports the fields: mode, model, prompt, permission, description, temperature, steps, top_p, color, hidden, variant, options (deep-merged), and disable.[1][2] Setting disable: true on an agent key in opencode.json deletes the built-in agent; a new key with no pre-existing agent creates a custom agent with mode: "all" and the merged default-plus-user permission set.[2] The prompt config option accepts a file reference using the syntax {file:./path/to/file.txt}, resolved relative to the config file's location; the model ID uses the format provider/model-id (e.g. anthropic/claude-sonnet-4-20250514).[1] The temperature option controls LLM response randomness; if unspecified, OpenCode uses model-specific defaults — typically 0 for most models and 0.55 for Qwen models.[1] The steps option caps the maximum number of agentic iterations; when the cap is reached, the agent is instructed to summarize its work and list remaining tasks. Without steps, iteration continues until the model stops or the user interrupts.[1] The maxSteps agent config field is deprecated — use steps instead. The tools agent config field is also deprecated in favor of permission, which offers more fine-grained control; agent-specific tools config overrides the global tools config.[1] The --permissions flag for opencode agent create accepts a comma-separated list of permissions to allow (bash, read, edit, glob, grep, webfetch, task, todowrite, websearch, lsp, skill); anything omitted is denied. The flag is also aliased as --tools.[3]
Example: configuring a custom subagent code-reviewer in opencode.json with a restricted permission set.[1]
Skills are reusable agent instructions stored in SKILL.md files and exposed to agents through the native skill tool; agents see available skills listed in the tool description and load them on-demand by calling skill({ name: "<skill-name>" }).[4] OpenCode searches for SKILL.md files in six locations: .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, and ~/.agents/skills/<name>/SKILL.md.[4] For project-local paths, OpenCode walks up from the current working directory to the git worktree root, loading any matching skills/*/SKILL.md files found along the way.[4]
Each SKILL.md must have YAML frontmatter with name (required) and description (required); license, compatibility, and metadata (string-to-string map) are optional. Unknown frontmatter fields are ignored.[4] Skill name must be 1–64 characters, lowercase alphanumeric with single-hyphen separators, must not start or end with -, must not contain --, and must match the directory name containing the SKILL.md. Equivalent regex: ^[a-z0-9]+(-[a-z0-9]+)*$.[4] Skill description must be 1–1024 characters.[4]
Skill access is controlled by pattern-based permissions in opencode.json under permission.skill; values are allow (loads immediately), deny (hidden from agent, access rejected), or ask (user prompted before loading). Wildcards are supported, e.g. internal-*.[4] Skill permissions can be overridden per-agent — for custom agents via frontmatter permission.skill, and for built-in agents via opencode.json under agent.<name>.permission.skill.[4] Setting tools.skill: false for an agent completely disables the skill tool; when disabled, the <available_skills> section is omitted entirely from the tool description.[4]
If a skill does not appear, common causes are: SKILL.md is not spelled in all caps, frontmatter is missing name or description, duplicate skill names across locations, or the skill has deny permission.[4]
The Agent.Info schema in packages/opencode/src/agent/agent.ts defines the full shape of an agent configuration, including name, description, mode (one of "subagent", "primary", or "all"), native, hidden, topP, temperature, color, permission, model (with modelID and providerID), variant, prompt, options, and steps.[2] The Agent.Service in packages/opencode/src/agent/agent.ts exposes interface methods get, list, defaultInfo, defaultAgent, and generate; the generate method accepts a description string and an optional model spec and returns { identifier, whenToUse, systemPrompt }, failing with Provider.DefaultModelError.[2] Agent.Service in packages/opencode/src/agent/agent.ts depends on Config.Service, Auth.Service, Plugin.Service, Skill.Service, Provider.Service, and LocationServiceMap.Service at construction time.[2] Whitelisted directories for the external_directory permission in packages/opencode/src/agent/agent.ts include Truncate.GLOB, the temp directory glob (Global.Path.tmp/*), all skill dirs, and all reference dirs, allowing agents to read/write these without prompting.[2] Agent prompts for explore, compaction, title, and summary are loaded from text files at build time (./prompt/explore.txt, ./prompt/compaction.txt, ./prompt/title.txt, ./prompt/summary.txt) and injected into the respective agent definitions in packages/opencode/src/agent/agent.ts.[2]
Sources
Updated
A session in OpenCode is a persistent document whose canonical shape (Info schema) holds metadata like id, title, model, tokens, cost, and summary; serialization, forking, listing, and event emission are coordinated through packages/opencode/src/session/session.ts. The SystemPrompt service synthesizes context for the AI agent by injecting environment details (working directory, git status, date, platform), available project references, model-specific branding, and merged permission-filtered MCP server instructions into the system prompt.
packages/opencode/src/session/session.ts defines the Info Schema as the canonical session data shape, including fields for id, slug, projectID, workspaceID, directory, parentID, title, agent, model, version, cost, tokens, summary, share, metadata, revert, permission, and time.[1] GlobalInfo in packages/opencode/src/session/session.ts extends Info with a nullable project field (ProjectInfo | null), used for cross-project session listings.[1] ArchivedTimestamp in packages/opencode/src/session/session.ts is typed as Schema.Finite (not NonNegativeInt) to remain permissive toward negative values accepted by the legacy HTTP API while rejecting non-finite values that cannot round-trip through JSON.[1] When a session's tokens field is absent during toRow serialization, packages/opencode/src/session/session.ts substitutes EmptyTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } as the default token count object.[1]
CreateInput in packages/opencode/src/session/session.ts is an optional struct whose fields — parentID, title, agent, model, metadata, permission, and workspaceID — are each individually optional.[1] ListInput in packages/opencode/src/session/session.ts accepts directory, scope (only "project" allowed), path, workspaceID, roots, start, search, and limit for project-scoped session listing.[1] GlobalListInput in packages/opencode/src/session/session.ts extends the project-scoped listing shape with the additional cursor and archived fields for cross-project session listing.[1]
isDefaultTitle(title) in packages/opencode/src/session/session.ts returns true when a session title matches the auto-generated pattern "New session - <ISO timestamp>" or "Child session - <ISO timestamp>".[1] getForkedTitle in packages/opencode/src/session/session.ts increments an existing (fork #N) suffix or appends (fork #1) to titles that do not already carry one.[1]
packages/opencode/src/session/session.ts stores plan files under .opencode/plans/ relative to the worktree when the project has VCS, or under the global data path plans/ directory otherwise.[1]
packages/opencode/src/session/session.ts re-exports session lifecycle events (Created, Updated, Deleted, Diff, Error) from SessionV1.Event, keeping the v2 session layer backward-compatible with v1 event consumers.[1] session.create emits a SessionNs.Event.Created event on the EventV2Bridge service containing the new session's id, projectID, directory, path, and title fields.[2] The session.created event is guaranteed to be emitted before the session.updated event — indexOf("created") must be less than indexOf("updated") on the same event bus.[2] On session creation, a legacy global sync payload is also emitted on GlobalBus with payload.type === "sync" and a syncEvent whose type is the versioned SessionNs.Event.Created.type, seq is 0, and aggregateID equals the session ID.[2] EventV2Bridge calls GlobalBus.on("event", listener) to bridge V2 events onto the legacy global event bus.[2] session.updatePart with a step-finish part emits a MessageV2.Event.PartUpdated event carrying the full token breakdown (input, output, reasoning, total, cache.read, cache.write) and cost.[2] The PartUpdated event payload is a distinct copy of the input — receivedPart is not reference-equal to the original partInput object.[2]
session.remove succeeds even when no active instance exists for that session ID — its Effect.exit resolves as Exit.isSuccess.[2] After session.remove, subsequent session.get calls for that ID fail — the session is no longer retrievable.[2] session.fork copies the parent session's metadata by default; the forked metadata is deep-equal to the parent's but is a distinct object (not reference-equal).[2] When session.create is called without a metadata argument, info.metadata and the persisted saved.metadata are both undefined.[2] session.fork determines which messages to include in the fork prefix using real chronological order via time.created rather than ID lexicographic order — a correctness fix for v1.18.15.[2] session.fork in packages/opencode/src/session/session.ts branches from a parent session at a chosen message boundary, carrying a prefix of the parent's message history into the forked session.
The SystemPrompt service in packages/opencode/src/session/system.ts exposes three methods: environment (returns env/model context lines), skills (returns skill instructions for an agent), and mcp (returns MCP server instructions for an agent).[3] The environment method in packages/opencode/src/session/system.ts injects the current working directory, workspace root, git repo status, platform, today's date, and the model's provider/API IDs into the system prompt.[3] When any project references with descriptions exist, environment appends an <available_references> XML block listing each reference's name, path, and description; the block is omitted entirely when the list is empty.[3] For the Muse model family, packages/opencode/src/session/system.ts substitutes {{MODEL_NAME}} in PROMPT_META with either "Muse Glimmer" or "Muse Spark" depending on the model API ID.[3] The skills method in packages/opencode/src/session/system.ts returns undefined (omitting the skills block) when the "skill" permission is disabled for the agent.[3] When skills are included, skills renders them with verbose: true to improve agent ingestion of skill information — the system prompt receives more detail than the tool description.[3] The mcp method in packages/opencode/src/session/system.ts merges agent-level and session-level permission rulesets via Permission.merge, then excludes any MCP server whose tools are all disabled by the combined ruleset.[3] The SystemPrompt service layer depends on Skill.Service, MCP.Service, and LocationServiceMap.Service, declared via LayerNode.make with Skill.node, MCP.node, and a local locationServiceMapNode as its dependencies.[3]
The session test suite builds its Effect layer by composing SessionNs.node, EventV2Bridge.node, SessionProjector.node, CrossSpawnSpawner.node, and InstanceStore.node via AppNodeBuilder.build / LayerNode.group.[2] The step-finish token propagation test carries a 30-second timeout, indicating the event round-trip may take significant time under load.[2]
Canonical example of awaiting a session event with a 2-second timeout using Effect.race in session tests:
const awaitDeferred = <T>(deferred: Deferred.Deferred<T>, message: string) =>
Effect.race(
Deferred.await(deferred),
Effect.sleep("2 seconds").pipe(Effect.flatMap(() => Effect.fail(new Error(message)))),
)
Regression tests for tab navigation are maintained in packages/app/e2e/regression/subagent-child-navigation.spec.ts to validate the tab context menu rendering in packages/app/src/components/titlebar-tab-nav.tsx. Regression tests for session rename are maintained in packages/app/e2e/regression/session-rename.spec.ts to validate the session rename interaction in packages/app/src/pages/session/timeline/message-timeline.tsx.
Sources
Updated
The Permission service manages tool-use approval: rules in opencode.json configure whether tools are allowed, denyed, or ask the user before execution, with later rules overriding earlier ones. When a tool requires approval, Permission publishes an Asked event, suspends execution on a Deferred, and resumes only after the user replies via Replied event or the session scope finalizes.
The Permission service lives in packages/opencode/src/permission/index.ts and manages tool-use permission requests: asking for approval, receiving user replies, and listing pending requests.[1] The Permission node export in packages/opencode/src/permission/index.ts declares EventV2Bridge.node as its only layer dependency: LayerNode.make({ service: Service, layer: layer, deps: [EventV2Bridge.node] }).[1]
The evaluate function in packages/opencode/src/permission/index.ts finds the LAST matching rule across all provided rulesets (using findLast), so later, more-specific rules take precedence over earlier ones; if no rule matches, it defaults to action: "ask" with pattern: "*".[1] Permission.ask in packages/opencode/src/permission/index.ts evaluates each pattern against the active ruleset in order: deny immediately returns a DeniedError; allow continues; any ask result suspends execution via an Effect Deferred until the user replies.[1] When Permission.ask suspends, packages/opencode/src/permission/index.ts publishes a Permission.Event.Asked event via EventV2Bridge before suspending and publishes Permission.Event.Replied after the user responds.[1] When a session's permission Deferred scope finalizes, all still-pending permission requests for that session are automatically failed with RejectedError and the pending map is cleared.[1]
The disabled helper in packages/opencode/src/permission/index.ts maps tool names to permission categories before evaluating rules: edit, write, and apply_patch map to the "edit" permission key; list_mcp_resources, list_mcp_resource_templates, and read_mcp_resource map to the "read" permission key; all other tools use their tool name directly.[1] disabled in packages/opencode/src/permission/index.ts marks a tool as disabled only when a matching rule has both pattern: "*" AND action: "deny" — partial-pattern denies do not hide the tool from the UI.[1] visibleTools in packages/opencode/src/permission/index.ts returns a filtered copy of a tools record, excluding any tools that disabled marks as globally denied, and is the canonical way to apply permission rules before presenting tools to a user.[1]
fromConfig in packages/opencode/src/permission/index.ts converts a ConfigPermissionV1.Info map into a flat PermissionV1.Rule[]: a string value is treated as an action applied to pattern: "*", while an object value maps each key as a specific pattern to an action.[1] The expand helper in packages/opencode/src/permission/index.ts resolves ~/, ~, $HOME/, and $HOME prefixes in permission patterns to the OS home directory at config-load time.[1] Tool permissions in opencode.json support wildcard patterns — for example, "mymcp_*": "ask" — allowing bulk permission assignment to all tools from an MCP server.[2]
Sources
Updated
A tool in OpenCode is a function the LLM can invoke, defined by a schema (Tool.Def), registration info (Tool.Info), and an execute handler; the tool registry filters and resolves them per model, agent, and permission. Tool definitions use Effect Schema for parameters and output, lazy initialization to defer setup, and a context object (Tool.Context) carrying session, message, permission-check functions, and metadata tracking for truncation.
The Tool.Def interface in tool.ts defines a tool's shape: id, description, parameters (Effect Schema decoder), optional jsonSchema override, execute function, and optional formatValidationError for custom schema-error prose.[1] The Tool.Info interface in tool.ts separates tool registration (id) from lazy initialization (init: () => Effect.Effect<DefWithoutID>), allowing tool definitions to be resolved on demand rather than eagerly.[1] Tool.init in tool.ts is a helper that resolves an Info to a full Def by calling info.init() and attaching the tool id.[1] The Tool.Context type in tool.ts carries sessionID, messageID, agent, abort signal, optional callID, optional extra map, messages array, a metadata updater effect, and an ask effect for requesting user permissions.[1] DynamicDescription in tool.ts is a function type (agent: Agent.Info) => Effect.Effect<string> used for tool descriptions that vary per-agent; it is marked as a temporary hack pending a cleaner abstraction.[1]
The Tool.define factory in packages/opencode/src/tool/tool.ts requires both Truncate.Service and Agent.Service as Effect context dependencies, resolving them once per tool definition.[1] In tool.ts, Schema.decodeUnknownEffect (the parameter parser) is compiled once per tool init call — not per LLM invocation — to avoid re-allocating the closure on every tool call.[1] InvalidArgumentsError in tool.ts is the typed error raised when the LLM calls a tool with arguments that fail the parameter schema; its message getter produces model-facing prose instructing the AI to rewrite the input, making it matchable upstream.[1] The wrap function in tool.ts calls agents.get(ctx.agent) to retrieve current agent configuration, then passes it to Truncate.Service to apply agent-specific output truncation limits; if result.metadata.truncated is already set, truncation is skipped.[1] When Truncate.Service truncates a tool's output, tool.ts adds truncated: true and, if applicable, outputPath to the result metadata so callers know the content was cut.[1] In packages/opencode/src/session/tools.ts, time.start is captured once at tool invocation start rather than reset on each log entry, ensuring elapsed-time fields correctly reflect actual tool duration for long-running tools. packages/opencode/test/session/tools.test.ts verifies that time.start is captured at tool invocation start and not overwritten on subsequent log entries.
packages/opencode/src/tool/registry.ts is the central tool registry; it initializes all built-in and custom plugin tools, and exposes Service (tagged @opencode/ToolRegistry) with ids, all, named, and tools methods.[2] The tools method on ToolRegistry.Service accepts providerID, modelID, agent, and optional permission (a PermissionV1.Ruleset) to return the filtered set of tool definitions for a given model invocation.[2] registry.ts uses InstanceState.make to manage per-instance tool registry state (custom and built-in tools), re-evaluating state per instance context.[2] The canonical built-in tool list order in registry.ts is: invalid, optionally question, shell, read, glob, grep, edit, write, task, fetch, todo, search, skill, patch, optionally execute (code mode), optionally lsp, optionally plan.[2] The question tool is included in the built-in list only when the client is "app", "cli", or "desktop", or when the enableQuestionTool flag is set; the lsp tool requires the experimentalLspTool flag; the plan (PlanExit) tool requires both experimentalPlanMode and client === "cli".[2] The execute (CodeMode) tool is conditionally loaded in registry.ts via a dynamic import of "./code-mode" only when the experimentalCodeMode runtime flag is set.[2] registry.ts determines whether web search is enabled based on provider ID (opencode or opencode-go) or the exa / parallel runtime flags, via the exported webSearchEnabled function.[2] The describeTask function in registry.ts filters available subagents by Permission.evaluate("task", item.name, agent.permission), excluding agents denied by the permission ruleset, then sorts them alphabetically.[2] A PermissionV1.Ruleset is a declarative set of allow/deny rules evaluated against a tool action and resource to determine whether a tool call is permitted before execution.
In registry.ts, custom tool files export tools at named exports; a default export uses the file's basename as the tool ID, while named exports use <basename>_<exportName> as the ID.[2] Plugin tools in registry.ts support both Zod-typed args and raw JSON Schema args: if all arg entries are Zod types, a Zod schema is used for validation; otherwise a legacy JSON Schema path is taken.[2] Plugin tools with missing args (pre-1.14.49 compatibility) are normalized to {} in registry.ts rather than passing undefined to Zod.[2] In registry.ts, the fromPlugin wrapper bridges the Effect-based ask permission function into a Promise-returning callback for plugin tools, using EffectBridge.make().[2]
All built-in tools are enabled by default and require no permission to run; tool behavior is controlled via the permission field in opencode.json — see Permissions.[3] The read tool reads files and supports specific line ranges for large files.[3] The glob tool searches for files using glob patterns and returns matching paths sorted by modification time.[3] The grep and glob tools use ripgrep internally; by default, ripgrep respects .gitignore patterns, excluding matched files and directories from searches.[3] To include .gitignore-excluded directories (e.g., node_modules/, dist/) in grep/glob searches, create a .ignore file in the project root with negation patterns like !node_modules/.[3] When handling tool.execute.before or tool.execute.after hooks for the patch tool, check input.tool === "apply_patch" (not "patch"). The tool uses output.args.patchText (not output.args.filePath); paths are embedded in marker lines within patchText and are relative to the project root.[3] The apply_patch tool in apply_patch.ts omits the move-path field entirely when it is absent or empty — rather than serializing an empty value — to produce structurally valid patch payloads; consumers must treat a missing move-path field as equivalent to an empty one.
Define typed tools with Effect Schema, stream a turn, dispatch tool calls locally with ToolRuntime.dispatch, and build follow-up history with LLM.updateRequest
const tools = {
get_weather: Tool.make({
description: "Get current weather for a city.",
parameters: Schema.Struct({ city: Schema.String }),
success: Schema.Struct({ forecast: Schema.String }),
execute: (input) => Effect.succeed({ forecast: `${input.city}: sunny, 72F` }),
}),
}
// Dispatch a tool-call event and build follow-up messages:
const dispatched = yield* ToolRuntime.dispatch(tools, event)
const followUp = LLM.updateRequest(request, {
messages: [
...request.messages,
Message.assistant([event]),
Message.tool({ ...event, result: dispatched.result }),
],
})
Define a named local tool with Tool.make, capturing services at construction time and using execute to sequence permission checks and domain logic.
const grep = Tool.make({
description: "Search file contents",
input: Input,
output: Output,
execute: (input, context) =>
Effect.gen(function* () {
const root = yield* filesystem.resolveRoot(input)
yield* permission.assert({
sessionID: context.sessionID,
agent: context.agent,
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
action: "grep",
resources: [input.pattern],
save: ["*"],
metadata: { root: root.resource },
})
return yield* filesystem.grep(input, root)
}).pipe(/* translate expected typed errors to ToolFailure */),
})
Register one or more tools by name using tools.register; the record key becomes the model-facing tool name.
yield * tools.register({
read,
write,
grep,
})
Sources
Updated
packages/opencode/src/tool/shell.ts implements the ShellTool, the built-in shell command execution tool, identified by ShellID.ToolID.[1] The tree-sitter parser in shell.ts — loaded lazily via the lazy utility on first use — initializes both bash and ps (PowerShell) grammars (tree-sitter-bash and tree-sitter-powershell) using web-tree-sitter, enabling static analysis of arguments and paths before execution.[1] shell.ts resolves WASM assets for tree-sitter grammars using fileURLToPath for file:// URLs or direct path detection for absolute paths, falling back to resolving relative URLs against import.meta.url.[1]
In shell.ts, the default shell command timeout is 2 * 60 * 1000 ms (2 minutes), overridable via the bashDefaultTimeoutMs runtime flag.[1] On Windows, shell.ts spawns PowerShell with -NoLogo -NoProfile -NonInteractive -Command flags; on other platforms it passes the command string to the shell via the shell option.[1] Child processes in shell.ts are spawned with detached: true on non-Windows platforms and detached: false on Windows.[1] On Windows, shell.ts calls ChildProcessSpawner to run cygpath -w (via the POSIX shell) to resolve POSIX-style paths like /usr/… to Windows absolute paths, catching any error and returning an empty array rather than propagating the failure.[1]
shell.ts recognizes a fixed set of directory-changing commands — cd, chdir, popd, pushd, push-location, set-location — for permission-check purposes.[1] shell.ts also recognizes a broader set of file-manipulating POSIX commands and PowerShell cmdlets for path-permission scanning; CMD.exe equivalents are held in a separate CMD_FILES set.[1] Before scanning for permission-relevant paths, shell.ts expands $env:VAR, ${env:VAR}, and $(HOME|PWD|PSHOME) variable references in shell arguments.[1] Tilde (~) in path arguments is expanded to the OS home directory by shell.ts, handling ~, ~/, and ~' prefixes.[1] On Windows, shell.ts performs case-insensitive environment variable lookup via envValue, scanning process.env keys case-insensitively to match Windows semantics.[1] The dynamic function in shell.ts detects dynamic argument expressions — subshells, variable expansions, and backticks — and skips static path scanning for those arguments to avoid false positives.[1]
The tail function in shell.ts truncates output from the end by iterating lines in reverse and stopping when either maxLines or maxBytes is exceeded; when a single line exceeds the byte budget, tail truncates that line at a UTF-8 character boundary.[1]
The shell tool requests a single "bash" permission whose patterns array contains each individual sub-command parsed from the full command string — for example, "echo foo" and "echo bar" for the input "echo foo && echo bar".[2] For PowerShell commands, the shell tool parses conditional chains (; and if ($?)) into individual permission patterns and populates always with wildcard cmdlet glob entries (e.g., "Write-Host *").[2] PowerShell always-allow patterns use the bare cmdlet name with a wildcard (e.g., "Remove-Item *"); flags such as -Recurse are stripped so the always-allow entry is not flag-specific.[2]
When the configured shell is terminal-only (e.g., fish), Shell.acceptable falls back to a supported shell rather than using fish, and the fallback shell name is reflected in the ShellTool's description.[2] Shell.acceptable and Shell.preferred maintain resettable caches — calling .reset() on either clears the cached selection so the next call re-evaluates the environment.[2]
The shell tool test layer in packages/opencode/test/tool/shell.test.ts is assembled from CrossSpawnSpawner.node, FSUtil.node, Plugin.node, Truncate.node, Config.node, Agent.node, and RuntimeFlags.node via Layer.mergeAll and LayerNode.compile.[2] On non-Windows platforms the shell test uses the single system Shell.acceptable() shell; on Windows it builds a deduped list covering bash (git bash fallback), pwsh, powershell, and cmd.exe.[2]
Sources
Updated
OpenCode uses the AI SDK and Models.dev to support 75+ LLM providers and local models.[1] Provider API keys added via /connect are stored in ~/.local/share/opencode/auth.json.[1] OpenCode Zen is a curated, team-tested set of models accessed via /connect by selecting OpenCode Zen and authenticating at opencode.ai/zen; it is recommended for users new to LLM providers.[1][2] OpenCode Go is a low-cost subscription plan for popular open coding models provided and tested by the OpenCode team, accessed via /connect by selecting OpenCode Go.[1] As of OpenCode 1.3.0, bundled plugins for using Claude Pro/Max subscriptions were removed — Anthropic explicitly prohibits this use; ChatGPT Plus, GitHub Copilot, and GitLab Duo subscriptions are supported with zero additional setup.[1] The AI SDK is a TypeScript library by Vercel that provides a unified interface for calling LLMs across different providers, abstracting provider-specific API differences. Eden AI is a supported LLM provider in OpenCode, listed in the provider reference documentation. The Console UI model-selection routes in packages/console/app/src/routes/go/index.tsx and packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx list Muse Spark 1.3 and Gemini 3.8 Flash as selectable inference models. The dialog-connect-provider.tsx component in packages/app/src/components/ signals device type as 'desktop' during the Console device-authentication v1 flow, preventing auth-flow branching errors and token-grant failures.
The provider baseURL option in opencode.json redirects any provider to a proxy service or custom endpoint.[1] The provider blacklist option removes specific model IDs from the /models picker; whitelist hides every model except those listed — both accept an array of model IDs identical to those shown in the picker, and the two options can be combined: whitelist narrows the set, then blacklist removes entries from it.[1] Custom OpenAI-compatible local providers are configured in opencode.json using an npm package (e.g., @ai-sdk/openai-compatible), a baseURL pointing to the local server, and a models map whose IDs must match the id values returned by GET /v1/models.[1]
Example: Configuring Atomic Chat as a custom OpenAI-compatible local provider in opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"atomic-chat": {
"npm": "@ai-sdk/openai-compatible",
"name": "Atomic Chat (local)",
"options": {
"baseURL": "http://127.0.0.1:1337/v1"
},
"models": {
"<your-model-id>": {
"name": "<your-model-name>"
}
}
}
}
}
An OpenAI-compatible provider is a local or third-party LLM server that implements the OpenAI REST API shape — including GET /v1/models and chat-completion endpoints — allowing OpenCode to communicate with it via the standard OpenAI adapter. Model normalization in packages/stats/core/src/domain/model-normalization.ts merges DeepSeek Flash API-level sub-variants under a single canonical name, preventing them from appearing as separate line items in inference statistics. The inference proxy in packages/console/app/src/lib/inference-proxy.ts routes model discovery requests for migrated models to the v1 endpoint, ensuring correct discovery in the Console UI.
Amazon Bedrock in opencode.json supports provider-level options keys region, profile, and endpoint (an alias for baseURL using AWS terminology); when both endpoint and baseURL are specified, endpoint takes precedence.[1] Amazon Bedrock authentication priority: a bearer token (AWS_BEARER_TOKEN_BEDROCK environment variable or token from /connect) takes precedence over the full AWS credential chain — profile, access keys, shared credentials, IAM roles, Web Identity Tokens, and instance metadata.[1] Amazon Bedrock supports Web Identity Tokens for EKS IRSA via AWS_WEB_IDENTITY_TOKEN_FILE / AWS_ROLE_ARN, which Kubernetes automatically injects when service account annotations are used.[1] For Amazon Bedrock custom inference profiles, set the models key under the amazon-bedrock provider using any model/provider name as the key, and set id to the profile ARN to ensure correct caching.[1]
The azure custom provider in packages/opencode/src/provider/provider.ts resolves the resource name in priority order from: provider options, API auth metadata, OAuth account ID, or the AZURE_RESOURCE_NAME environment variable; if none is found and no baseURL is set, all model calls throw a descriptive error.[3] For Azure OpenAI, the deployment name in Azure AI Foundry must match the model name for OpenCode to work properly.[1] If Azure OpenAI returns "I'm sorry, but I cannot assist with that request" errors, the fix is to change the Azure content filter from DefaultV2 to Default.[1] Azure model auto-discovery has been removed from packages/opencode/src/plugin/azure.ts; Azure-backed providers now require explicit model enumeration in opencode.json configuration.
The anthropic custom provider entry in packages/opencode/src/provider/provider.ts always sets autoload: false and injects the beta headers interleaved-thinking-2025-05-14 and fine-grained-tool-streaming-2025-05-14 on every request.[3] The openai custom provider in packages/opencode/src/provider/provider.ts uses the responses endpoint by default (via sdk.responses(modelID)) and sets headerTimeout to 300_000 ms (5 minutes).[3] The github-copilot custom provider in packages/opencode/src/provider/provider.ts selects the responses endpoint for GPT-5 and higher (excluding gpt-5-mini), the chat endpoint for earlier GPT models, and honours a model-level api.endpoint override when present.[3] The @ai-sdk/github-copilot bundled provider in packages/opencode/src/provider/provider.ts is loaded from the internal @opencode-ai/core/github-copilot/copilot-provider module rather than a true @ai-sdk/github-copilot npm package.[3] When no API key or auth is found for the opencode provider in packages/opencode/src/provider/provider.ts, all paid models (those with non-zero cost.input) are removed from the model list, and options: { apiKey: "public" } is set as a fallback, enabling access to the free tier only.[3] The Bedrock Mantle model selector in packages/opencode/src/provider/provider.ts uses the chat endpoint for openai.gpt-oss-safeguard-20b and openai.gpt-oss-safeguard-120b, and the responses endpoint for all other models.[3] The patched @ai-sdk/amazon-bedrock dependency accepts none as a valid reasoning-effort value, correcting a rejection by the upstream SDK. Patched @ai-sdk/anthropic and @ai-sdk/amazon-bedrock dependencies carry reasoning and replay fixes that are load-bearing for extended-thinking and replay-dependent sessions routed through Anthropic or Bedrock. The provider transform layer in packages/opencode/src/provider/transform.ts and the session processor in packages/opencode/src/session/processor.ts tolerate Anthropic thinking-block bindings that previously caused hard failures. The thinking-block binding in packages/opencode/src/provider/transform.ts is gated to Claude 5.1 and later; Claude 3.x and 4.x models do not receive thinking-block injection, preventing API errors on older versions. opencode.json configuration can opt out of thinking-block injection even on eligible Claude 5.1+ models.
The googleVertexEndpoint helper in packages/opencode/src/provider/provider.ts maps "global" to aiplatform.googleapis.com, the continental multi-regions "eu"/"us" to REP domains (aiplatform.{loc}.rep.googleapis.com), and all other locations to regional domains ({location}-aiplatform.googleapis.com).[3] The googleVertexAnthropicBaseURL helper in packages/opencode/src/provider/provider.ts generates a Regional Endpoint Platform (REP) base URL only for the eu and us continental multi-regions; all other locations return undefined.[3]
timeoutController in packages/opencode/src/provider/provider.ts returns an AbortController that automatically fires a ProviderError.HeaderTimeoutError after ms milliseconds; callers must invoke the returned clear() to cancel the timeout on success.[3] wrapSSE in packages/opencode/src/provider/provider.ts wraps an SSE (text/event-stream) response body with a per-chunk read timeout; if a chunk is not received within ms milliseconds, it aborts the controller with a ProviderError.ResponseStreamError and cancels the reader.[3] wrapSSE in packages/opencode/src/provider/provider.ts is a no-op (returns the original response) when ms is not a positive number, when the response has no body, or when the content-type header does not include text/event-stream.[3] The provider integration layer in packages/opencode/src/provider/provider.ts and the AI SDK wrapper in packages/core/src/aisdk.ts catch and suppress cancel-triggered rejections from the SSE reader, preventing unhandled promise rejections when aborting in-flight LLM streams. The header timeout (time-to-first-token limit) for LLM provider connections defaults to 300 seconds (5 minutes), defined across packages/core/src/v1/config/provider.ts, packages/opencode/src/provider/provider.ts, packages/sdk/openapi.json, and packages/sdk/js/src/v2/gen/types.gen.ts. The per-chunk streaming timeout for LLM provider connections defaults to 300 seconds (5 minutes), defined across packages/core/src/v1/config/provider.ts, packages/opencode/src/provider/provider.ts, packages/sdk/openapi.json, and packages/sdk/js/src/v2/gen/types.gen.ts.
The LLM service in packages/opencode/src/session/llm.ts is registered under the Effect context tag "@opencode/LLM" and exposes a single stream method that accepts a StreamInput and returns Stream.Stream<LLMEvent, unknown>.[4] StreamInput in packages/opencode/src/session/llm.ts requires user, sessionID, model, agent, system, messages, and tools; optional fields include parentSessionID, permission, small, retries, and toolChoice ("auto" | "required" | "none").[4] The live LLM layer in packages/opencode/src/session/llm.ts depends on Auth.Service, Config.Service, Provider.Service, Plugin.Service, Permission.Service, EventV2Bridge.Service, LLMClientService, and RuntimeFlags.Service.[4] packages/opencode/src/session/llm.ts re-exports OUTPUT_TOKEN_MAX from ProviderTransform.OUTPUT_TOKEN_MAX.[4]
packages/opencode/src/session/llm.ts calls LLMRequestPrep.prepare to assemble the request (messages, tools, system prompt, params, headers) before dispatching to either runtime.[4] packages/opencode/src/session/llm.ts calls LLMNativeRuntime.stream to attempt native execution, and falls back to LLMAISDK / streamText when the result is not "supported".[4] OpenTelemetry tracing in packages/opencode/src/session/llm.ts is opt-in via cfg.experimental?.openTelemetry; when enabled, the tracer proxy injects session.id onto every span via setAttribute.[4] In packages/opencode/src/session/llm.ts, tools that pass the permission ruleset without an "ask" action are pre-approved for GitLab Workflow sessions via workflowModel.sessionPreapprovedTools, preventing repeated approval prompts for server-side MCP tools.[4] The GitLab Workflow approvalHandler in packages/opencode/src/session/llm.ts auto-approves tools already approved within the same session (tracked in approvedToolsForSession) to prevent infinite approval loops.[4]
Configure the OpenAI provider with an API key and generation defaults, then select a model
const model = OpenAI.configure({
apiKey,
generation: { maxTokens: 160 },
providerOptions: {
openai: { store: false },
},
}).model("gpt-4o-mini")
Build a provider-neutral LLMRequest with generation options and provider-native options via LLM.request
const request = LLM.request({
model,
system: "You are concise and practical.",
prompt: "Tell me a joke",
generation: { maxTokens: 80, temperature: 0.7 },
providerOptions: {
openai: { promptCacheKey: "tutorial-joke" },
},
})
Generate a single LLM response and access the collected text and usage via LLM.generate
const response = yield* LLM.generate(request)
console.log("generated text:", response.text)
console.log("usage", Formatter.formatJson(response.usage, { space: 2 }))
Stream LLM output as incremental LLMEvents using LLM.stream, handling text-delta and finish events
const streamText = LLM.stream(request).pipe(
Stream.tap((event) =>
Effect.sync(() => {
if (event.type === "text-delta") process.stdout.write(`\ntext: ${event.text}`)
if (event.type === "finish") process.stdout.write(`\nfinish: ${event.reason}\n`)
}),
),
Stream.runDrain,
)
Generate a typed structured object from a Schema using LLM.generateObject; falls back to synthetic tool call for cross-provider compatibility
const WeatherReport = Schema.Struct({
city: Schema.String,
forecast: Schema.String,
highFahrenheit: Schema.Number,
})
const response = yield* LLM.generateObject({
model,
system: "Return only structured weather data.",
prompt: "Give me today's weather for San Francisco.",
schema: WeatherReport,
generation: { maxTokens: 120, temperature: 0 },
})
console.log(Formatter.formatJson(response.object, { space: 2 }))
Inspect the compiled request pipeline (route, body, URL, auth) without sending a network request using LLMClient.prepare
const prepared = yield* LLMClient.prepare(
LLM.request({
model: FakeEcho.configure().model("tiny-echo"),
prompt: "Show me the provider pipeline.",
}),
)
console.log("route:", prepared.route)
console.log("body:", Formatter.formatJson(prepared.body, { space: 2 }))
Provide the LLM runtime layer (HTTP + WebSocket executors) to an Effect program
const requestExecutorLayer = RequestExecutor.fetchLayer
const llmDeps = Layer.mergeAll(requestExecutorLayer, WebSocketExecutor.layer)
const llmClientLayer = LLMClient.layer.pipe(Layer.provide(llmDeps))
const program = Effect.gen(function* () {
yield* streamWithTools
}).pipe(Effect.provide(Layer.mergeAll(llmDeps, llmClientLayer)))
Effect.runPromise(program)
Sources
Updated
The OpenCode JS/TS SDK is published as @opencode-ai/sdk on npm and provides a type-safe client for programmatically controlling the OpenCode server.[1] Install the SDK with npm install @opencode-ai/sdk.[1] All TypeScript types (Session, Message, Part, etc.) are generated from the server's OpenAPI specification and importable directly from @opencode-ai/sdk.[1]
createOpencode() starts both a server and a client; it accepts hostname (default 127.0.0.1), port (default 4096), signal, timeout (default 5000 ms), and config options.[1] createOpencodeClient() creates a client-only connection to an already-running OpenCode server, accepting baseUrl (default http://localhost:4096), fetch, parseAs, responseStyle (data or fields, default fields), and throwOnError (default false).[1] The SDK exposes client.global.health(), client.app.log(), client.app.agents(), client.project.list(), client.project.current(), client.path.get(), client.config.get(), and client.config.providers() as top-level API methods.[1]
Example: using createOpencode() to start an embedded server, override the model, log the server URL, then shut it down.
import { createOpencode } from "@opencode-ai/sdk"
const opencode = await createOpencode({
hostname: "127.0.0.1",
port: 4096,
config: {
model: "anthropic/claude-3-5-sonnet-20241022",
},
})
console.log(`Server running at ${opencode.server.url}`)
opencode.server.close()
Creates an OpenCode server and client, then creates a session and sends a multi-part prompt (file + text) using the SDK
const server = await createOpencodeServer()
const client = createOpencodeClient({ baseUrl: server.url })
const session = await client.session.create()
await client.session.prompt({
path: { id: session.data.id },
body: {
parts: [
{ type: "file", mime: "text/plain", url: pathToFileURL(file).href },
{ type: "text", text: `Write tests for every public function in this file.` },
],
},
})
Fans out concurrent sessions over a list of files using Promise.all, each with its own session.create() call
await Promise.all(
input.map(async (file) => {
const session = await client.session.create()
await client.session.prompt({
path: { id: session.data.id },
body: {
parts: [
{ type: "file", mime: "text/plain", url: pathToFileURL(file).href },
{ type: "text", text: `Write tests for every public function in this file.` },
],
},
})
}),
)
Structured output is requested by passing a format field with type: "json_schema" and a JSON Schema schema to session.prompt(); the model uses a StructuredOutput tool internally to produce validated JSON.[1] The retryCount field on a json_schema format request controls how many validation retries the SDK attempts, defaulting to 2.[1]
If the model fails to produce valid structured output after all retries, the response contains a StructuredOutputError accessible at result.data.info.error:
if (result.data.info.error?.name === "StructuredOutputError") {
console.error("Failed to produce structured output:", result.data.info.error.message)
console.error("Attempts:", result.data.info.error.retries)
}
Create an Effect-Drizzle SQLite database service backed by an in-memory SQLite client using EffectDrizzleSqlite.makeWithDefaults
const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
const sqliteLayer = SqliteClient.layer({ filename: ":memory:", disableWAL: true })
class Database extends Context.Service<Database, DatabaseShape>()("@opencode/example/Database") {
static layer = Layer.effect(Database, makeDatabase).pipe(Layer.provide(sqliteLayer))
}
Run Drizzle migrations as an Effect using EffectDrizzleSqlite.migrate with a migrations folder path
yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder: `${import.meta.dirname}/migrations` }).pipe(
Effect.mapError((cause) => new UserStoreError({ message: "Failed to migrate users", cause })),
)
Run a Drizzle transaction as an Effect using db.transaction, with configurable isolation behavior
yield* db
.transaction(
Effect.fnUntraced(function* (tx) {
yield* tx.insert(users).values({ name: from })
yield* tx.update(users).set({ name: to }).where(eq(users.name, from))
}),
{ behavior: "immediate" },
)
.pipe(Effect.asVoid, Effect.mapError(mapStoreError("Failed to rename user")))
The Effect-Drizzle SQLite integration is optional and targets projects already using the Effect framework and Drizzle ORM; it is not required to use the core SDK or session API.
Parses a single locale argument and returns default model, variant, and flag values
parseTranslationArgs(["fr"])
// => { target: "fr", concurrency: 1, model: "opencode/gpt-5.5", variant: "xhigh", dryRun: false, check: false, help: false }
Builds an agent translation config that disables share/formatter/lsp and scopes edit permissions to the target locale file
const config = translationConfig("translate-app-fr", "opencode/gpt-5.5", ["packages/app/src/i18n/fr.ts"])
// config.share === "disabled"
// config.formatter === false
// config.lsp === false
// config.agent["translate-app-fr"].permission.edit === { "*": "deny", "packages/app/src/i18n/fr.ts": "allow" }
Runs async tasks with bounded concurrency using runPool, returning results in input order
const result = await runPool([1, 2, 3, 4, 5], 2, async (item) => {
await Bun.sleep(5)
return item * 2
})
// result === [2, 4, 6, 8, 10], max concurrent === 2
Detects missing keys, extra keys, and placeholder mismatches between source and translated objects
findDrift(
{ keep: "Hello {{name}}", missing: "Missing", changed: "{{one}} {{two}}" },
{ keep: "Bonjour {{name}}", extra: "Extra", changed: "{{one}}" },
)
// => { missing: ["missing"], extra: ["extra"], placeholders: ["changed"] }
Sources
Updated
OpenCode's CI runs unit and e2e tests on Linux and Windows via a test workflow triggered on pushes to dev, pull requests, and manual dispatch, with OS-specific test commands and a shared Turbo build cache keyed on configuration and commits. The workflow manages concurrency differently per branch—dev runs use unique groups to preserve history, while PRs share groups per ref to cancel stale runs—and pins Node 24.15 for e2e to avoid Playwright hangs with newer Node versions.
The CI test workflow runs on pushes to dev, pull requests, and manual dispatch, targeting both Linux (blacksmith-4vcpu-ubuntu-2404) and Windows (blacksmith-4vcpu-windows-2025) for both the unit and e2e jobs.[1] On the dev branch, the concurrency group is unique per run (keyed on github.run_id) so that cancelled checks do not pollute the default-branch commit history; PRs and other branches share a group per PR or ref and cancel stale runs.[1]
Unit-test jobs set up Node 24; e2e jobs set up Node 24.15. Bun is provisioned for both via the shared local action ./.github/actions/setup-bun.[1] Turbo build cache is stored in node_modules/.cache/turbo and keyed on the OS, a hash of turbo.json and all package.json files, and the commit SHA, with OS-scoped fallback restore keys.[1]
Unit tests run with GITHUB_ACTIONS=false bun turbo test and time out after 20 minutes. On Windows, OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER=true is set automatically to avoid file-watcher issues.[1] On Linux only, CI additionally runs bun run check:generated in packages/client to validate the generated client, then bun run test:httpapi in packages/opencode to exercise HttpApi gates.[1]
E2e tests use Playwright and run only Chromium. Node 24.15 (not 24.16) is pinned because Playwright 1.59 hangs while extracting Chromium under Node 24.16.[1] E2e tests are launched with bun --cwd packages/app test:e2e:local and time out after 30 minutes. Playwright artifacts (results and report) are uploaded and retained for 7 days on every run, including failures.[1]
Sources