---
name: opencode-cli-audit-optimization
version: 1.1.0
description: "Audit, optimize, and extend OpenCode CLI — token cost reduction, plugin installation, MCP server configuration, compaction/caching, and security hardening."
tags:
  - opencode
  - token-optimization
  - mcp
  - plugins
  - configuration
---

# OpenCode CLI Audit & Optimization

Systematic approach to auditing an OpenCode installation and applying the full optimization stack: token savings, web browsing, MCP servers, security, and config tuning.

## When to Use

- User asks to "audit my opencode setup"
- User wants to "reduce token costs" or "optimize opencode"
- User wants to add "web browsing" or "MCP servers" to OpenCode
- User asks about "opencode web" or "opencode server" or the web UI
- Setting up OpenCode fresh and want best-practice config
- Diagnosing a crashed or memory-leaking OpenCode server process

## Prerequisites

- OpenCode installed (`which opencode`, `opencode --version`)
- Config at `~/.config/opencode/opencode.json` (Linux global path)
- Node.js/npm available if installing npm-based plugins
- `npx` available for MCP server commands (bundled with Node.js)
- **OpenCode has a web UI** — do NOT assume it's CLI-only. Use `opencode web --help` first if a user asks about it.

## Workflow

### 1. Inventory Current State

Check version, existing providers, plugins, MCPs, agents, and config validity:

```bash
opencode --version
opencode debug config
ls ~/.config/opencode/plugins/
ls ~/.config/opencode/agents/ 2>/dev/null
ls ~/.config/opencode/.opencode/ 2>/dev/null
```

Read the active config file(s). Note: OpenCode reads both `.json` and `.jsonc` and merges them (global + project config stack).

#### 1a. Multi-Source API Key Audit

**Critical: API keys can live in THREE separate places, and a stale key in ANY of them breaks the provider.** Always check ALL sources:

| Source | Path | Purpose |
|--------|------|---------|
| OpenCode config | `~/.config/opencode/opencode.json` | Provider `apiKey` in `options` block |
| Env exports | `~/.bashrc`, `~/.zshrc`, `~/.profile` | `export DEEPSEEK_API_KEY=...` shell overrides |
| Hermes env | `~/.hermes/.env` | `DEEPSEEK_API_KEY=...` Hermes provider config |

```bash
# Extract all API keys from opencode.json (grep raw, bypass redaction)
grep -oP '"apiKey":\s*"\K[^"]+' ~/.config/opencode/opencode.json

# Check for env-var overrides in shell config (different key may be here!)
grep -rn "DEEPSEEK_API_KEY\|OPENROUTER_API_KEY\|NVIDIA_NIM_API_KEY" ~/.bashrc

# Check Hermes env
grep "API_KEY" ~/.hermes/.env

# Check the running OpenCode server process environment
cat /proc/$(pgrep -f "opencode serve" | head -1)/environ 2>/dev/null | tr '\0' '\n' | grep -i "DEEPSEEK\|OPENROUTER\|API_KEY"
```

**Key-consistency check:** After collecting keys, verify each provider's key has the same value across all sources. Different keys in different files means at least one is stale — ask the user which is current, or test them independently against the API endpoint.

**Truncated-key detection:** Real API keys are 30-70+ chars. If a key is exactly 13 chars matching `sk-xxx...xxxx` pattern (e.g. `sk-5f9...c61b`), it's a display-truncated placeholder that was pasted instead of the real key. Fix: overwrite with the real key from `.env` or ask the user.

**Restart requirement after config fix:** OpenCode server (`opencode serve`) loads its config at process start and caches it in memory. After fixing API keys in the config file, the running server won't pick up the change until restarted:

```bash
kill $(pgrep -f "opencode serve")  # graceful stop
opencode serve --port 4096 --hostname 0.0.0.0 &  # restart
```

Verify with `opencode debug config` and a chat completion test against the previously-failing provider.

### 2. Identify Gaps

Score these areas:

| Area | Check |
|------|-------|
| **Plugins** | Any installed? (openslimedit, context-cache, opencode-ignore, DCP are the core 4) |
| **MCPs** | Any configured? (filesystem, git, sequential-thinking, time, memory, fetch are built-in starters) |
| **Web search** | built-in Exa only? Need Brave/Tavily/Firecrawl/Context7 MCPs |
| **Compaction** | `compaction.auto`, `compaction.prune`, `compaction.reserved` in config? |
| **Context caching** | `setCacheKey: true` on providers? context-cache plugin installed? |
| **Small model** | `small_model` configured for cheap tasks (title gen)? |
| **Security** | `.opencodeignore` exists? Secret/artifact files blocked? |
| **Context limits** | Per-model limits set to ~90% of max (leaves headroom for output)? |

### 3. Install Core Plugins

```bash
# openslimedit — 21-45% token reduction (tool description compression)
npm install openslimedit@latest

# opencode-ignore — .ignore-pattern file access blocking
npm install opencode-ignore@latest

# opencode-context-cache — 98% cache hit rate via stable SHA256 keys
curl -sL -o ~/.config/opencode/plugins/opencode-context-cache.mjs \
  "https://raw.githubusercontent.com/JackDrogon/opencode-context-cache/main/plugins/opencode-context-cache.mjs"

# Dynamic Context Pruning — auto-compresses stale context, deduplication
opencode plugin @tarquinen/opencode-dcp@latest --global
```

Add all plugins to `opencode.json`:
```jsonc
"plugin": [
    "openslimedit@latest",
    "opencode-ignore",
    "./plugins/opencode-context-cache.mjs",
    "@tarquinen/opencode-dcp@latest"
]
```

### 4. Configure MCP Servers

Built-in (enable by name):
```jsonc
"mcp": {
    "filesystem": { "enabled": true },
    "git": { "enabled": true },
    "sequential-thinking": { "enabled": true },
    "time": { "enabled": true },
    "memory": { "enabled": true },
    "fetch": { "enabled": true }
}
```

Key-gated search MCPs (requires API keys):
```jsonc
"brave-search": {
    "type": "local",
    "command": ["npx", "-y", "@modelcontextprotocol/server-brave-search"],
    "env": { "BRAVE_SEARCH_API_KEY": "YOUR_KEY" }
},
"tavily-search-local": {
    "type": "local",
    "command": ["npx", "-y", "tavily-mcp"],
    "env": { "TAVILY_API_KEY": "YOUR_KEY", "TAVILY_HUMAN_ID": "opencode" }
},
"firecrawl": {
    "type": "local",
    "command": ["npx", "-y", "firecrawl-mcp"],
    "env": { "FIRECRAWL_API_KEY": "YOUR_KEY" }
}
```

**Remote MCP alternatives** (simpler, no local npx subprocess):
```jsonc
// Tavily remote
"tavily-search": {
    "type": "remote",
    "url": "https://mcp.tavily.com/mcp/?tavilyApiKey=YOUR_KEY"
},
// Firecrawl remote (keyless free tier at /v2/mcp)
"firecrawl": {
    "type": "remote",
    "url": "https://mcp.firecrawl.dev/YOUR_KEY/v2/mcp"
}
```

Free, no-key MCP:
```jsonc
"context7": {
    "type": "local",
    "command": ["npx", "-y", "@upstash/context7-mcp"]
}
```

**⚠️ API key injection**: `${VAR}` in MCP `env` is NOT interpolated by OpenCode — the literal string is passed as-is. Hardcode keys directly. Two approaches:

**A) Python direct write** (preferred — avoids shell mangling of special characters like dashes, underscores, mixed case):
```python
import json
tavily_key = "tvly-dev-..."
firecrawl_key = "fc-a97bf55-..."
brave_key = "BSA0Fa..."

with open("~/.config/opencode/opencode.json") as f:
    data = json.load(f)

data["mcp"]["brave-search"]["env"]["BRAVE_SEARCH_API_KEY"] = brave_key
data["mcp"]["tavily-search"]["env"]["TAVILY_API_KEY"] = tavily_key
data["mcp"]["firecrawl"]["env"]["FIRECRAWL_API_KEY"] = firecrawl_key

with open("~/.config/opencode/opencode.json", "w") as f:
    json.dump(data, f, indent=2)
```

**B) sed with sourced env vars** (when keys have no problematic characters):
```bash
source ~/.config/opencode/.env
sed -i "s|\${TAVILY_API_KEY}|$TAVILY_API_KEY|g" ~/.config/opencode/opencode.json
sed -i "s|\${FIRECRAWL_API_KEY}|$FIRECRAWL_API_KEY|g" ~/.config/opencode/opencode.json
```

**C) Persist in bashrc**:
```bash
echo 'export BRAVE_SEARCH_API_KEY="your-key"' >> ~/.bashrc
echo 'export TAVILY_API_KEY="your-key"' >> ~/.bashrc
echo 'export FIRECRAWL_API_KEY="your-key"' >> ~/.bashrc
```

### 5. Add Compaction & Caching

```jsonc
"compaction": {
    "auto": true,
    "prune": true,
    "reserved": 20000
}
```

Add `"setCacheKey": true` to every provider's `options` block.

### 6. Set Small Model

```jsonc
"small_model": "provider-name/model-name"
```

Use the fastest cheapest model available (e.g., local nemotron-term-14b, OpenRouter free-tier).

### 7. Create .opencodeignore

Block secrets, build artifacts, generated files:
```ignore
.env*
*.key *.pem *.crt
**/node_modules/** **/dist/** **/build/** **/.cache/**
**/venv/** **/.git/**
package-lock.json *.min.js *.min.css *.map
*.log
```

### 8. Tune Model Context Limits

Reduce per-model `context` to ~85-90% of the model's max, leaving room for output + overhead:
- 1M window → 950K
- 128K window → 120K
- 32K window → 28K
- 256K window → 240K

### 9. GitHub MCP Server (Optional — Needs PAT)

For PR review, issue management, repo operations. Two modes:

**Remote** (recommended — simpler):
```jsonc
"github": {
    "type": "remote",
    "url": "https://api.githubcopilot.com/mcp/",
    "oauth": false,
    "headers": { "Authorization": "Bearer {env:GITHUB_PAT}" }
}
```
Set `GITHUB_PAT` env var. **Token concern**: GitHub MCP adds many tools. Restrict to specific agents:
```jsonc
"tools": { "github_*": false },
"agent": { "github-helper": { "tools": { "github_*": true } } }
```

**Local** (Docker): See [github-mcp-server install-opencode.md](https://github.com/github/github-mcp-server/blob/main/docs/installation-guides/install-opencode.md)

### 10. Add Modern CLI Tools (Makes bash Tool More Effective)

Installing modern replacements makes the agent's `bash` tool output cleaner and more useful:

```bash
sudo apt-get install -y ripgrep fd-find fzf bat
sudo ln -sf /usr/bin/batcat /usr/local/bin/bat   # batcat→bat symlink
sudo ln -sf /usr/bin/fdfind /usr/local/bin/fd     # fdfind→fd symlink
```

These give the agent: `rg` (fast grep), `fd` (fast find), `fzf` (fuzzy filter), `bat` (syntax-highlighted cat).

### 11. LSP Configuration (Per-Project, Not Global)

OpenCode has 33+ built-in LSP servers. Enable per-project — **do NOT enable globally** (LSP adds recall tokens on every edit, can slow agent workflows):

```jsonc
// In project-level opencode.json:
"lsp": {
    "typescript": { "disabled": false },
    "pyright": { "disabled": false }
}
```

**Better than LSP for most projects**: Have the agent run lint/typecheck CLI tools directly. Errors feed back without LSP tradeoffs. Document these commands in AGENTS.md.

### 12. Custom Subagents (Cheaper Specialized Workers)

Create `.md` files in `~/.config/opencode/agents/`. File name = agent name. Use cheap local models for focused tasks:

```markdown
---
description: Reviews code for quality, security, and performance
mode: subagent
model: local-gateway/nemotron-term-14b
temperature: 0.1
permission:
  edit: deny
  bash: deny
---
You are a code reviewer. Focus on:
- Security vulnerabilities and hardcoded secrets
- Performance bottlenecks and N+1 queries
- Error handling and edge cases
- Code style and maintainability
Do NOT make changes. Provide actionable feedback.
```

### 13. Generate Project AGENTS.md (/init)

In any project directory, run `/init` in OpenCode. It scans the repo and creates a project-specific AGENTS.md with build/test commands, architecture, and conventions. This improves agent accuracy significantly.

### 14. Custom Tools (.opencode/tools/)

For reusable operations beyond built-in tools, create TypeScript files in `.opencode/tools/`. File name becomes tool name. Can invoke any language via bash. See [opencode.ai/docs/custom-tools](https://opencode.ai/docs/custom-tools).

### 15. Web UI / Server Mode

OpenCode has a built-in web UI. Do NOT default to saying "it's CLI-only" — verify with `opencode web --help`.

```bash
# Start web UI (random port, 127.0.0.1, opens browser)
opencode web

# Specify port and host
opencode web --port 4096 --hostname 0.0.0.0

# Server mode (no browser auto-open, headless)
opencode serve --port 4096 --hostname 0.0.0.0
```

**Flags:**
| Flag | Description | Default |
|------|-------------|---------|
| `--port` | Port to listen on | `0` (random) |
| `--hostname` | Bind address | `127.0.0.1` |
| `--mdns` | Enable mDNS discovery (sets hostname to `0.0.0.0`) | `false` |
| `--mdns-domain` | Custom mDNS domain | `opencode.local` |
| `--cors` | Additional CORS domains | `[]` |
| `--print-logs` | Print logs to stderr | `false` |
| `--log-level` | Log level | — |

**Authentication:**
Set `OPENCODE_SERVER_PASSWORD` env var for HTTP basic auth. Username defaults to `opencode`; override with `OPENCODE_SERVER_USERNAME`.

**Systemd service:**
```ini
[Unit]
Description=OpenCode Server
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/opencode serve --port 4096 --hostname 0.0.0.0
User=rurouni
Group=rurouni
Environment=OPENCODE_SERVER_PASSWORD=your-password
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
```

**Troubleshooting:**
- Service status: `systemctl status opencode-server`
- Logs: `journalctl -u opencode-server -n 40`
- If process was killed, check for event listener memory leaks (see Pitfalls)
- Check listening port: `ss -tlnp | grep opencode`

## Pitfalls

- **WRONG MCP PACKAGE NAMES (commonly wrong):**
  - `@anthropic/brave-search-mcp` **does not exist** → use `@modelcontextprotocol/server-brave-search` or `brave-search-mcp`
  - `@tavily/mcp` **does not exist** → use `tavily-mcp`
  - `@firecrawl/mcp` **does not exist** → use `firecrawl-mcp`
  - Verify by running `npm view <package-name>` before adding to config.
- **`default` and `models` root-level keys are INVALID** in current OpenCode schema. Use `"model": "provider/model-id"` instead. Model aliases are not a built-in feature.
- **`small_model` expects a STRING** (`"provider/model"`), not an object.
- **DCP must be installed via `opencode plugin`** CLI command, not just npm — it also adds config to `tui.json`.
- **context-cache plugin must be explicitly listed** in the `plugin` array in `opencode.json` — copying the .mjs file alone is not enough.
- **Event listener memory leak in long-running server**: OpenCode (Bun runtime) can leak EventTarget listeners over time. `MaxListenersExceededWarning: Possible EventTarget memory leak detected` is the symptom — process memory can grow to 50+ GB over a week. Use `systemctl status` to check the `Mem peak` column. Mitigation: periodic restart via systemd `Restart=always` + `RestartSec` with a weekly cron or systemd timer, or set `NODE_OPTIONS='--max-old-space-size=4096'` style limits in the service environment.
- **`opencode debug config` may hang** if config is valid but model list is large — use `timeout 5` on it.
- **Config file precedence** (low→high): remote → global (`~/.config/opencode/opencode.json`) → custom (OPENCODE_CONFIG) → project root → .opencode/ → inline (OPENCODE_CONFIG_CONTENT). Conflicting keys in higher-precedence files override (not merge deeply).
  - **.json and .jsonc in the same directory merge** — OpenCode reads both at the same precedence level. Keep everything in one file to avoid confusion.
- **Brave Search MCP v2.x** removed base64 image data (was causing huge context bloat in v1.x).
- **`${VAR}` in MCP `env` is NOT shell-interpolated by OpenCode.** OpenCode passes the literal string `${VAR}` to the MCP subprocess, not the env var value. Fix: source the env file, then use `sed` to replace placeholders with actual values, or hardcode keys directly.
- **Keys with special characters (dashes, underscores, mixed case) can be mangled** when passing through shell heredocs and f-strings. Use Python JSON direct write to avoid corruption.
- **Truncated placeholder keys stored literally in config** — The config file can end up with a display-truncated value like `sk-5f9...c61b` (13 chars) instead of the real 35+ char key. This happens when someone pastes the redacted display value from a dashboard or log instead of the raw key. Symptom: provider shows "Authentication Fails" or 401 in tests. Fix: extract the real key from `.env` (or ask the user) and overwrite the truncated value in `opencode.json` via Python JSON write. Always verify key length — real keys are 30-70+ chars, truncated ones are exactly 13 chars (`sk-xxx...xxxx` pattern).
- **`.json` and `.jsonc` in the same config dir merge silently** — keep all settings in one file to prevent surprising overrides.
- **LSP enabled globally adds tokens on every edit** (recall overhead). Better to run lint/typecheck CLI tools directly for most projects. Enable LSP per-project only.
- **DCP deduplication + purgeErrors** are enabled by default but can be tuned: `deduplication.protectedTools` and `purgeErrors.turns` in `~/.config/opencode/dcp.jsonc`.

## Verification

```bash
opencode debug config     # should return valid JSON, no errors
opencode mcp list         # verify all MCP servers connect without errors
ls plugins/*.mjs          # context-cache plugin file present
ls plugins/node_modules/openslimedit/  # npm plugins installed
which rg fd fzf bat       # modern CLI tools available
```

## References

- `references/opencode-plugins.md` — full plugin list with URLs, descriptions, token savings
- `references/opencode-mcp-servers.md` — MCP server details, keys, install commands
