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

system

Adapts to terminal colors (default)

catppuccin

Catppuccin Latte/Mocha (our pick)

catppuccin-macchiato

Catppuccin Macchiato variant

tokyonight

Tokyo Night

everforest

Everforest

ayu

Ayu

gruvbox

Gruvbox

kanagawa

Kanagawa

nord

Nord

matrix

Green-on-black terminal aesthetic

one-dark

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

Diff Style

{
  "diff": {
    "style": "unified"
  }
}

Controls how file diffs are displayed in the TUI.

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"
}
Theme Description Palette

system

Adapts to terminal colors

Terminal default

catppuccin

Catppuccin Latte/Mocha (auto dark/light)

Pastel warm

catppuccin-macchiato

Catppuccin Macchiato variant

Pastel cool

tokyonight

Tokyo Night

Blue/purple

everforest

Everforest

Green/earth

ayu

Ayu

Orange/amber

gruvbox

Gruvbox

Warm retro

kanagawa

Kanagawa (inspired by The Great Wave)

Blue/cream

nord

Nord

Arctic blue

matrix

Green-on-black terminal aesthetic

Green monochrome

one-dark

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

primary / secondary / accent

UI chrome colors (borders, highlights, focus)

error / warning / success

Status message colors

text / background / border / muted

Base UI colors

diff.added / diff.removed / diff.changed

File diff visualization

markdown.*

Markdown rendering in conversation

syntax.*

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

ctrl+x → n

New session

ctrl+x → s

Share current session

ctrl+x → q

Quit OpenCode

ctrl+x → h

Session history/picker

ctrl+x → ?

Show help / keybinding reference

Model & Agent

Keybind Action

Tab

Toggle between Build and Plan mode

ctrl+x → m

Model picker (switch model)

ctrl+x → a

Agent picker (switch agent)

ctrl+x → t

Theme picker

ctrl+x → p

Provider info (active model, cost)

Input Editing (Readline/Emacs Style)

Keybind Action

ctrl+a

Move to beginning of line

ctrl+e

Move to end of line

ctrl+w

Delete word backward

ctrl+u

Delete to beginning of line

ctrl+k

Delete to end of line

ctrl+d

Delete character under cursor

alt+b

Move word backward

alt+f

Move word forward

Navigation (Vim-Inspired)

Keybind Action

j / k

Scroll down/up in message list

h / l

Collapse/expand in list views

g / G

Jump to top/bottom

Page Up / Page Down

Page scroll

Undo/Redo

Keybind Action

ctrl+z

Undo last file change

ctrl+y

Redo last undo

/undo

Undo via command (equivalent)

/redo

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

Ctrl+Shift+S

Ctrl+Enter

Model picker

Ctrl+P

Ctrl+X → M

Toggle thinking

Ctrl+T

N/A (per-model config)

History search

Ctrl+R

Ctrl+X → H

External editor

Ctrl+E

N/A (inline editing)

Toggle mode

N/A

Tab

Undo

N/A

Ctrl+Z

Customizable

Partial (keybindings.json)

Full (tui.json)

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

description

Shown in mode picker

tools.allow

Tools available in this mode

tools.deny

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

Tab key or /mode command

@agent-name or agent picker

Persistence

Session-wide until toggled

Per-invocation (subagents) or session-wide (primary)

Custom

.opencode/modes/

.opencode/agents/

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.