Skip to content

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

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.

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:

{
  "$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:

{
  "$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:

{
  "$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:
{ "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:
rules:
  opencode-config-valid:
    extra-keys:
      - somethingNew      # a new top-level key
      - elicitation       # a new key on an MCP server

Configuration

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.