OpenCode: Config, TUI, Themes, Keybindings, Modes & Plugins
Configuration: opencode.json
Location: ~/.config/opencode/opencode.json (global) + <repo>/opencode.json (project)
Supports JSON with comments (.jsonc). Add "$schema": "https://opencode.ai/config.json" for IDE validation.
Core Structure
{
"$schema": "https://opencode.ai/config.json",
"provider": { }, // LLM provider configurations
"agent": { }, // Custom agent definitions
"mcp": { }, // MCP server connections
"permission": { }, // Tool allow/deny/ask rules
"instructions": [ ], // Additional instruction file paths
"snapshot": true, // Enable file change tracking for undo/redo
"share": { // Session sharing configuration
"provider": "opencode"
}
}
Provider Configuration Pattern
Each provider follows a common structure:
{
"provider": {
"<provider-name>": {
"id": "provider-id",
"api": {
"apiKey": "{env:PROVIDER_API_KEY}",
"baseURL": "https://api.provider.com/v1"
},
"models": {
"<model-alias>": {
"id": "model-id",
"name": "Display Name",
"attachment": true, // Supports file attachments
"reasoning": true, // Extended thinking capable
"temperature": 0,
"context_length": 200000
}
}
}
}
}
Permissions
{
"permission": {
"allow": [
"bash(git *)",
"bash(make *)",
"read(**/*.adoc)",
"edit(**/*.adoc)"
],
"deny": [
"bash(rm -rf *)",
"read(.env*)",
"read(~/.secrets/*)"
],
"ask": [
"bash(docker *)"
]
}
}
Permission modes:
-
"allow"— Always permitted, no confirmation -
"deny"— Always blocked -
"ask"— Prompt user for confirmation
Supports glob patterns for file paths and command prefixes.
Instructions
Additional instruction files beyond AGENTS.md:
{
"instructions": [
".opencode/context/*.md",
"~/.config/opencode/global-rules.md"
]
}
Glob patterns supported. Files are injected into the system prompt alongside AGENTS.md.
Snapshot System
{
"snapshot": true
}
When enabled (default), OpenCode tracks all file changes for /undo and /redo support. Creates snapshots before each file modification, enabling safe rollback of any edit.
Configuration: tui.json
Location: ~/.config/opencode/tui.json
Controls the terminal UI appearance and behavior. Schema: opencode.ai/tui.json
Core Settings
{
"$schema": "https://opencode.ai/tui.json",
"theme": "catppuccin",
"keybinds": { },
"scroll": {
"speed": 3,
"page_fraction": 0.8
},
"diff": {
"style": "unified"
}
}
Theme Selection
{
"theme": "catppuccin"
}
Built-in themes:
| Theme | Style |
|---|---|
|
Adapts to terminal colors (default) |
|
Catppuccin Latte/Mocha (our pick) |
|
Catppuccin Macchiato variant |
|
Tokyo Night |
|
Everforest |
|
Ayu |
|
Gruvbox |
|
Kanagawa |
|
Nord |
|
Green-on-black terminal aesthetic |
|
Atom One Dark |
Custom themes can be added via .opencode/themes/ or ~/.config/opencode/themes/.
Scroll Behavior
{
"scroll": {
"speed": 3,
"page_fraction": 0.8
}
}
-
speed— Lines per scroll event -
page_fraction— Fraction of viewport for page up/down
Themes
OpenCode ships with 11+ built-in themes and supports fully custom themes via JSON files.
Active Theme
Catppuccin Mocha — Consistent with the rest of the Domus ecosystem (Neovim, Hyprland, Waybar, tmux, Antora UI).
// tui.json
{
"theme": "catppuccin"
}
Built-in Theme Gallery
| Theme | Description | Palette |
|---|---|---|
|
Adapts to terminal colors |
Terminal default |
|
Catppuccin Latte/Mocha (auto dark/light) |
Pastel warm |
|
Catppuccin Macchiato variant |
Pastel cool |
|
Tokyo Night |
Blue/purple |
|
Everforest |
Green/earth |
|
Ayu |
Orange/amber |
|
Gruvbox |
Warm retro |
|
Kanagawa (inspired by The Great Wave) |
Blue/cream |
|
Nord |
Arctic blue |
|
Green-on-black terminal aesthetic |
Green monochrome |
|
Atom One Dark |
Muted rainbow |
Custom Theme Creation
Location: .opencode/themes/ or ~/.config/opencode/themes/
// ~/.config/opencode/themes/catppuccin-domus.json
{
"name": "catppuccin-domus",
"colors": {
"primary": "#cba6f7", // Mauve
"secondary": "#a6e3a1", // Green
"accent": "#f5c2e7", // Pink
"error": "#f38ba8", // Red
"warning": "#fab387", // Peach
"success": "#a6e3a1", // Green
"text": "#cdd6f4", // Text
"background": "#1e1e2e", // Base
"border": "#585b70", // Surface2
"muted": "#6c7086", // Overlay0
"diff": {
"added": "#a6e3a1",
"removed": "#f38ba8",
"changed": "#f9e2af"
},
"markdown": {
"heading": "#cba6f7",
"code": "#a6e3a1",
"link": "#89b4fa",
"bold": "#f5c2e7",
"italic": "#f5c2e7"
},
"syntax": {
"keyword": "#cba6f7",
"string": "#a6e3a1",
"number": "#fab387",
"comment": "#6c7086",
"function": "#89b4fa",
"type": "#f9e2af",
"variable": "#cdd6f4",
"operator": "#89dceb"
}
}
}
Color Format Support
-
Hex —
"#cba6f7"(standard) -
ANSI 256 —
"178"(terminal-compatible) -
Named —
"red","blue"(basic terminals) -
"none"— Transparent (use terminal default)
Theme Sections
| Section | Controls |
|---|---|
|
UI chrome colors (borders, highlights, focus) |
|
Status message colors |
|
Base UI colors |
|
File diff visualization |
|
Markdown rendering in conversation |
|
Code block syntax highlighting |
Dark/Light Variants
Themes can define separate palettes for dark and light terminals:
{
"colors": {
"dark": {
"background": "#1e1e2e",
"text": "#cdd6f4"
},
"light": {
"background": "#eff1f5",
"text": "#4c4f69"
}
}
}
Keybindings
OpenCode uses a leader key system to avoid terminal keybinding conflicts. All keybindings are fully customizable in tui.json.
Leader Key
Default: ctrl+x
The leader key acts as a prefix for multi-key sequences, similar to tmux prefix or Vim leader:
ctrl+x → n # New session
ctrl+x → s # Share session
ctrl+x → q # Quit
Default Keybindings
Session Management
| Keybind | Action |
|---|---|
|
New session |
|
Share current session |
|
Quit OpenCode |
|
Session history/picker |
|
Show help / keybinding reference |
Model & Agent
| Keybind | Action |
|---|---|
|
Toggle between Build and Plan mode |
|
Model picker (switch model) |
|
Agent picker (switch agent) |
|
Theme picker |
|
Provider info (active model, cost) |
Input Editing (Readline/Emacs Style)
| Keybind | Action |
|---|---|
|
Move to beginning of line |
|
Move to end of line |
|
Delete word backward |
|
Delete to beginning of line |
|
Delete to end of line |
|
Delete character under cursor |
|
Move word backward |
|
Move word forward |
Navigation (Vim-Inspired)
| Keybind | Action |
|---|---|
|
Scroll down/up in message list |
|
Collapse/expand in list views |
|
Jump to top/bottom |
|
Page scroll |
Undo/Redo
| Keybind | Action |
|---|---|
|
Undo last file change |
|
Redo last undo |
|
Undo via command (equivalent) |
|
Redo via command (equivalent) |
Custom Keybinding Configuration
// tui.json
{
"keybinds": {
"leader": "ctrl+x",
"chat": {
"submit": "ctrl+enter",
"new_session": "ctrl+x n",
"share": "ctrl+x s",
"model_picker": "ctrl+x m",
"agent_picker": "ctrl+x a",
"toggle_mode": "tab"
},
"global": {
"quit": "ctrl+x q",
"help": "ctrl+x ?",
"history": "ctrl+x h"
}
}
}
Disabling Keybindings
Set any keybind to "none" to disable:
{
"keybinds": {
"chat": {
"share": "none"
}
}
}
Comparison with Claude Code Keybindings
| Action | Claude Code | OpenCode |
|---|---|---|
Submit prompt |
|
|
Model picker |
|
|
Toggle thinking |
|
N/A (per-model config) |
History search |
|
|
External editor |
|
N/A (inline editing) |
Toggle mode |
N/A |
|
Undo |
N/A |
|
Customizable |
Partial ( |
Full ( |
Modes
Modes define tool/permission presets that agents operate under. OpenCode ships with 2 built-in modes and supports custom modes.
Built-in Modes
| Mode | Tool Access | Use Case |
|---|---|---|
Build |
Full (read, write, edit, bash, etc.) |
Default mode. Active coding, file modification, command execution. |
Plan |
Read-only (read, grep, glob, list, lsp) |
Architecture review, code analysis, planning. No file modifications, no bash execution. |
Mode Toggle
Press Tab in the TUI to toggle between Build and Plan modes.
The active mode appears in the TUI status bar. Mode changes take effect immediately for the current agent.
Custom Mode Definition
Location: .opencode/modes/ or ~/.config/opencode/modes/
---
description: Read-only mode for AsciiDoc documentation review
tools:
allow:
- read
- grep
- glob
- list
- skill
deny:
- write
- edit
- bash
- apply_patch
---
# Review Mode
You are in review mode. You can read, search, and analyze files but cannot modify anything.
Focus on:
- Convention violations
- Missing attributes
- Broken cross-references
- Structural issues
Report findings as a numbered list with file:line references.
Mode Properties
| Property | Description |
|---|---|
|
Shown in mode picker |
|
Tools available in this mode |
|
Tools blocked in this mode |
Markdown body |
Additional system prompt when mode is active |
Planned Custom Modes
| Mode | Purpose | Tool Access |
|---|---|---|
Review |
Documentation review and audit — read-only with reporting focus |
Read-only |
Secure |
Paranoid mode — no bash, no write, no network |
Read + grep + glob only |
Local-Only |
Force local model usage — no cloud API calls |
Full tools, Ollama models only |
Cost-Aware |
Prefer cheapest model per task, warn before expensive operations |
Full tools, model routing logic |
Mode vs Agent
| Concept | Mode | Agent |
|---|---|---|
Scope |
Tool restrictions and system prompt modifier |
Complete behavior definition with model, tools, and persona |
Toggle |
|
|
Persistence |
Session-wide until toggled |
Per-invocation (subagents) or session-wide (primary) |
Custom |
|
|
Example |
"Review mode: read-only analysis" |
"adoc-linter: AsciiDoc convention checker using Ollama" |
Plugin System
OpenCode’s plugin system uses JavaScript/TypeScript modules with a rich event hook architecture. This is a significant upgrade from Claude Code’s bash-only hook system.
Plugin Architecture
Plugins are loaded from:
-
.opencode/plugins/— Project-scoped -
~/.config/opencode/plugins/— Global -
npm packages referenced in config
Plugins are automatically installed via Bun at startup.
Plugin File Structure (Actual API)
// .opencode/plugins/asciidoc-validator.ts
export const AsciidocValidator = async ({ project, client, $, directory, worktree }) => {
// Context parameters:
// project -- current project information
// client -- OpenCode SDK client (for logging, etc.)
// $ -- Bun's shell API for running commands
// directory -- working directory
// worktree -- git worktree path
return {
// Hook: After file is edited
"tool.execute.after": async (event) => {
if (event.input.tool === "edit" &&
event.output.args.filePath?.endsWith(".adoc")) {
// Validate attributes against antora.yml
client.app.log("info", "Validating AsciiDoc attributes...")
}
},
// Hook: Before tool execution
"tool.execute.before": async (event) => {
if (event.input.tool === "bash" &&
event.output.args.command?.includes("rm -rf")) {
client.app.log("warn", "Blocked destructive operation")
// Return deny to block
}
},
}
};
npm Plugin Packages
{
"plugin": [
"opencode-helicone-session",
"opencode-wakatime",
"@my-org/custom-plugin"
]
}
npm plugins install automatically via Bun to ~/.cache/opencode/node_modules/.
Plugin Dependencies
// .opencode/package.json
{
"dependencies": {
"shescape": "^2.1.0"
}
}
OpenCode runs bun install automatically before loading plugins.
Logging
client.app.log("debug", "Detailed info")
client.app.log("info", "General info")
client.app.log("warn", "Warning message")
client.app.log("error", "Error occurred")
=== Event Hooks (30+ Events)
Organized by category from the actual OpenCode plugin API:
==== Command Events
[cols="1,2"]
|===
| Event | When
| `command.executed`
| A slash command was executed
|===
==== File Events
[cols="1,2"]
|===
| Event | When
| `file.edited`
| A file was modified (edit, write, patch)
| `file.watcher.updated`
| File watcher detected external change
|===
==== Session Events
[cols="1,2"]
|===
| Event | When
| `session.created`
| New session started
| `session.updated`
| Session state changed
| `session.compacted`
| Context was compacted (summarized)
| `session.deleted`
| Session was deleted
| `session.diff`
| File diff generated
| `session.error`
| Session error occurred
| `session.idle`
| Session became idle
| `session.status`
| Session status changed
|===
==== Message Events
[cols="1,2"]
|===
| Event | When
| `message.updated`
| Message content changed
| `message.removed`
| Message was deleted
| `message.part.updated`
| Part of a message changed
| `message.part.removed`
| Part of a message removed
|===
==== Tool Events
[cols="1,2"]
|===
| Event | When
| `tool.execute.before`
| Before tool execution (can intercept)
| `tool.execute.after`
| After tool execution
|===
==== Permission Events
[cols="1,2"]
|===
| Event | When
| `permission.asked`
| User was prompted for permission
| `permission.replied`
| User responded to permission prompt
|===
==== Other Events
[cols="1,2"]
|===
| Event | When
| `server.connected`
| Client connected to server
| `todo.updated`
| Todo list changed
| `shell.env`
| Shell environment variable set
| `lsp.client.diagnostics`
| LSP diagnostics received
| `lsp.updated`
| LSP state changed
| `installation.updated`
| OpenCode installation updated
| `tui.prompt.append`
| Text appended to TUI input
| `tui.command.execute`
| TUI command executed
| `tui.toast.show`
| Toast notification displayed
|===
==== Experimental Hooks
[cols="1,2"]
|===
| Hook | Purpose
| `experimental.session.compacting`
| Customize what context is preserved during auto-compaction
|===
=== Plugin vs Claude Code Hooks Comparison
[cols="1,2,2"]
|===
| Aspect | Claude Code Hooks | OpenCode Plugins
| Language
| Bash scripts only
| JavaScript/TypeScript
| Event model
| 5 lifecycle events
| 9+ event types with richer payloads
| Blocking
| Exit code 2 blocks action
| Return `deny: true` with reason
| Custom tools
| Not possible
| Plugins can define new tools
| State
| Stateless between invocations
| Persistent module state
| Dependencies
| System packages
| npm ecosystem (auto-installed via Bun)
| Debugging
| Shell output parsing
| Full JS debugging, stack traces
| Distribution
| Copy files
| npm packages or git-managed files
|===
=== Planned Plugins
[cols="1,2,1"]
|===
| Plugin | Purpose | Priority
| `asciidoc-validator`
| Validate {attributes} against antora.yml after every .adoc edit
| P1
| `auto-backup`
| Snapshot files before edit/write (mirrors Claude Code PreToolUse hook)
| P1
| `security-guard`
| Block sensitive file access, warn on credential staging
| P1
| `shellcheck-runner`
| Run ShellCheck on .sh/.bash/.zsh after edits
| P2
| `session-logger`
| Log all tool calls and model switches for cost tracking
| P2
| `cost-tracker`
| Estimate token usage and cost per provider per session
| P2
|===
=== Plugin Development Workflow
[source,bash]
Create plugin directory
mkdir -p .opencode/plugins
Write plugin
nvim .opencode/plugins/my-plugin.ts
OpenCode auto-discovers and loads on next session start
No registration needed — file existence is sufficient
NOTE: Plugins have full access to the Node.js/Bun runtime. A malicious plugin could execute arbitrary code. Only install plugins you trust or have authored yourself. // Partial: Auto-formatter system // Included by: pages/projects/personal/opencode/customization.adoc == Auto-Formatters OpenCode automatically formats files after every `edit`, `write`, and `apply_patch` operation. This is a feature Claude Code does not have -- formatters run transparently on every file modification. === Built-in Formatters (27) [cols="1,2,2"] |=== | Formatter | Extensions | Requirement | `air` | `.R` | `air` command | `biome` | `.js`, `.jsx`, `.ts`, `.tsx`, `.html`, `.css`, `.md`, `.json`, `.yaml` | `biome.json(c)` config present | `cargofmt` | `.rs` | `cargo fmt` command | `clang-format` | `.c`, `.cpp`, `.h`, `.hpp`, `.ino` | `.clang-format` config present | `cljfmt` | `.clj`, `.cljs`, `.cljc`, `.edn` | `cljfmt` command | `dart` | `.dart` | `dart` command | `dfmt` | `.d` | `dfmt` command | `gleam` | `.gleam` | `gleam` command | `gofmt` | `.go` | `gofmt` command | `htmlbeautifier` | `.erb`, `.html.erb` | `htmlbeautifier` command | `ktlint` | `.kt`, `.kts` | `ktlint` command | `mix` | `.ex`, `.exs`, `.eex`, `.heex` | `mix` command | `nixfmt` | `.nix` | `nixfmt` command | `ocamlformat` | `.ml`, `.mli` | `ocamlformat` + `.ocamlformat` config | `ormolu` | `.hs` | `ormolu` command | `oxfmt` | `.js`, `.jsx`, `.ts`, `.tsx` | Experimental (env var flag required) | `pint` | `.php` | `laravel/pint` in `composer.json` | `prettier` | `.js`, `.jsx`, `.ts`, `.tsx`, `.html`, `.css`, `.md`, `.json`, `.yaml` | `prettier` in `package.json` | `rubocop` | `.rb`, `.rake`, `.gemspec`, `.ru` | `rubocop` command | `ruff` | `.py`, `.pyi` | `ruff` command + config | `rustfmt` | `.rs` | `rustfmt` command | `shfmt` | `.sh`, `.bash` | `shfmt` command | `standardrb` | `.rb`, `.rake`, `.gemspec`, `.ru` | `standardrb` command | `terraform` | `.tf`, `.tfvars` | `terraform` command | `uv` | `.py`, `.pyi` | `uv` command | `zig` | `.zig`, `.zon` | `zig` command |=== === Configuration ==== Disable All Formatters [source,jsonc]
{ "formatter": false }
==== Disable Specific Formatter [source,jsonc]
{ "formatter": { "prettier": { "disabled": true } } }
==== Custom Formatter [source,jsonc]
{ "formatter": { "custom-markdown": { "command": ["deno", "fmt", "$FILE"], "extensions": [".md"] }, "prettier": { "command": ["npx", "prettier", "--write", "$FILE"], "environment": { "NODE_ENV": "development" }, "extensions": [".js", ".ts", ".jsx", ".tsx"] } } }
The `$FILE` placeholder is replaced with the absolute path of the file being formatted. === Relevant Formatters for Our Stack [cols="1,2,1"] |=== | Formatter | Our Use Case | Install | `shfmt` | Shell scripts (`.sh`, `.bash`) | `pacman -S shfmt` | `ruff` | Python files (`.py`) | `uv tool install ruff` | `rustfmt` | Rust files (`.rs`) | Bundled with `rustup` | `prettier` | JSON, YAML, Markdown | `npm install -D prettier` | `gofmt` | Go files | Bundled with Go |=== NOTE: Formatters run silently after every file modification. This means code is always clean without manual intervention -- a significant advantage over Claude Code where formatting is manual.