# opencode-config-valid opencode.json and opencode.jsonc must parse and use keys and MCP server shapes OpenCode reads | | | |---|---| | **Severity** | error (auto) | | **Autofix** | - | | **Since** | v0.20.0 | | **Repo Types** | opencode | | **Category** | [OpenCode](opencode.md) | ## Why `opencode.json` is where an OpenCode project declares its MCP servers, its agents and its slash commands. It ships in the repository, so a shape its loader rejects stops OpenCode from starting for everyone on the team, and a misspelled key fails quietly: OpenCode does not find the key it wanted and carries on with a default. An MCP server whose `command` was written as a string rather than an argv array makes OpenCode refuse to start at all. OpenCode configuration schemas have evolved: 1.x spellings (`agent`, `command`, `permission`, flat MCP server declarations) and 2.0 spellings (`agents`, `commands`, `permissions`, nested `mcp.servers`) are both supported. OpenCode merges `agent`/`agents` and `command`/`commands` by entry name. Different names may appear in both sections; only conflicting definitions of the same name are reported. One-to-one renamed settings still accept either spelling but report when both are present. MCP servers may be named `servers` or `timeout`, including a bare 1.x `{"enabled": false}` toggle. The value shape distinguishes these entries from the 2.0 server map and global timeout. Nested servers named `type`, `command` or `enabled` retain their own entries. The MCP servers declared in OpenCode configuration are also evaluated by policy rules such as [`mcp-prohibited`](mcp-prohibited.md). ## Severity Errors — OpenCode refuses to load a project configuration with any of these, so `opencode` exits before it starts: - The top-level document is not an object, or the file has syntax errors (comments and trailing commas in `.jsonc` are supported). - An agent or command section that is not an object, an entry that is not an object, a missing or non-string `template`, or an entry field of the wrong type. - An MCP server with a missing or unknown `type`, a `command` that is not a non-empty array of strings, a non-string or empty `url`, an `environment`, `headers` or `oauth` that is not an object (`oauth: false` is the documented way to switch OAuth off), a `timeout` that is neither a number nor an object of `startup`/`catalog`/`execution`/`request`, or a non-boolean `enabled`/`disabled`. - Committed credentials or secrets detected in MCP server URLs, headers, or environment mappings. The one shape OpenCode tolerates is a server carrying a boolean `enabled`: the 1.x `mcp` union has a bare `{"enabled": …}` toggle branch that ignores other properties, so a broken server with `enabled` loads as a toggle and simply never starts. Its shape findings are warnings. Warnings — the file loads, but one setting is dead: both spellings of one renamed key (including the 1.x and 2.0 OAuth field names), a server declared under both layouts at once, a `$schema` that is not a string, and a `$schema` pointing at `https://opencode.ai/tui.json`, which describes `tui.json` rather than this file. Information-level findings never fail a build: - An unrecognized top-level key. OpenCode's schema changes weekly, so a key this release has not heard of is more likely new than wrong — `extra-keys` accepts it without waiting for a skillsaw release. - An unrecognized key on an MCP server. Same reasoning, same remedy: `extra-keys` covers these too. - A `$schema` that is neither the documented URL nor the TUI one. A vendored or mirrored copy is legitimate, so this is a note rather than a defect. ## Examples **Bad** — a Claude-shaped MCP server in an OpenCode config, and a file that declares one setting twice: ```json { "$schema": "https://opencode.ai/config.json", "agent": { "reviewer": { "prompt": "Review this change." } }, "agents": { "reviewer": { "system": "Review this change." } }, "mcp": { "playwright": { "type": "stdio", "command": "npx", "args": ["-y", "@playwright/mcp@latest"] } } } ``` `type: "stdio"` is not a transport OpenCode knows, `command` must be the argv array, there is no `args` key, and `agent`/`agents` define `reviewer` differently. OpenCode merges those sections and keeps the `agent` entry when names overlap. The unknown transport is reported first and on its own: the rest of a server's shape depends on which transport it is, so those checks resume once `type` is fixed and the file is linted again. **Good, 1.x spelling** — comments and a trailing comma are fine: ```jsonc { "$schema": "https://opencode.ai/config.json", // Local servers are spawned directly, so command is argv. "mcp": { "playwright": { "type": "local", "command": ["npx", "-y", "@playwright/mcp@latest"], "enabled": true, } }, "agent": { "reviewer": { "description": "Reviews a diff for correctness bugs", "prompt": "Review this change.", "disable": false } } } ``` **Good, 2.0 spelling** — the same configuration after migrating: ```json { "$schema": "https://opencode.ai/config.json", "mcp": { "servers": { "playwright": { "type": "local", "command": ["npx", "-y", "@playwright/mcp@latest"], "disabled": false, "timeout": { "catalog": 30000, "execution": 30000 } } } }, "agents": { "reviewer": { "description": "Reviews a diff for correctness bugs", "system": "Review this change.", "disabled": false } } } ``` ## How to fix - Give every MCP server a `type` of `local` or `remote`. A `local` server needs `command` as a non-empty array of strings; a `remote` server needs a `url`. The first command element must name an executable; later arguments may be empty or whitespace strings and are passed through unchanged. - A command entry must include a string `template`. An empty template is valid, including commands whose prompt is supplied by a plugin. - Entries with different names may be split between `agent`/`agents` or `command`/`commands`; OpenCode merges them. Keep only one definition when the same name occurs in both sections. For one-to-one settings, keep one spelling: `prompt` or `system`, `enabled` or `disabled`. `enabled` and `disabled` are the same switch with the sense inverted, so a server carrying both is saying two different things. - Replace a committed credential with OpenCode's substitution syntax: ```json { "headers": { "Authorization": "Bearer {env:MY_API_KEY}" } } ``` `{env:VAR}` and `{file:./path}` both work, and skillsaw recognises them as placeholders. - For a key newer than this skillsaw release, accept it without waiting. One list covers both places a key can be unrecognized — the top level and an MCP server entry: ```yaml rules: opencode-config-valid: extra-keys: - somethingNew # a new top-level key - elicitation # a new key on an MCP server ``` ## Configuration ```yaml rules: opencode-config-valid: enabled: auto # true | false | auto severity: error ``` | Parameter | Description | Default | |-----------|-------------|---------| | `extra-keys` | Additional config keys to accept, at the top level or on an MCP server entry, for keys newer than this skillsaw release | `[]` | *Run `skillsaw explain opencode-config-valid` to see this documentation and the rule's effective configuration in your terminal.*