# 配置系统

> 九层配置来源与合并顺序、JSONC 与变量替换、Markdown 定义的 agent 与命令、独立的 tui.json，以及通往 V2 的兼容层。

- 作者：David（道雾轩）
- 专栏：OpenCode 源码解读（https://daiw.org/manual/opencode.md）
- 最后更新：2026-09-15
- 原文：https://daiw.org/manual/opencode/config
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

现役（V1）配置的加载逻辑在 `packages/opencode/src/config/`，字段 schema 在 `packages/core/src/v1/config/`，新内核的版本是 `packages/core/src/config.ts`。先看「写在哪、谁覆盖谁」，再看实现里几处值得留意的设计。

## 九层来源

`loadInstanceState()`（`packages/opencode/src/config/config.ts:328`）按下表顺序深度合并，后者覆盖前者：

| 顺序 | 来源 | 位置 |
| --- | --- | --- |
| 1 | 远端 well-known | 每个 `wellknown` 类型认证凭据对应的 `<url>/.well-known/opencode` |
| 2 | 全局配置 | 全局目录（默认 `~/.config/opencode/`）下依次读 `config.json`、`opencode.json`、`opencode.jsonc` |
| 3 | 自定义文件 | 环境变量 `OPENCODE_CONFIG` 指向的文件 |
| 4 | 项目配置 | 从 git 工作树根到当前目录逐层的 `opencode.json(c)`，越近越优先 |
| 5 | `.opencode` 目录 | 各级 `.opencode/`、`~/.opencode/`、`OPENCODE_CONFIG_DIR` 中的 `opencode.json(c)` |
| 6 | 内联配置 | 环境变量 `OPENCODE_CONFIG_CONTENT` 里的整段 JSON |
| 7 | 控制台组织 | 登录控制台并选定组织时拉取的 `<url>/api/config` |
| 8 | 托管目录 | macOS `/Library/Application Support/opencode`，Windows `%ProgramData%\opencode`，其他 `/etc/opencode` |
| 9 | macOS MDM | `ai.opencode.managed` 偏好域的 plist，注释写着“override everything” |

几个细节：

- 同一目录下 `.jsonc` 晚于 `.json` 合并，并存时 `.jsonc` 胜出；
- 合并用 remeda 的 `mergeDeep`，只有 `instructions` 拼接去重；`plugin` 按包名（本地文件按 URL）去重、后声明者胜，并记下来源文件与作用域（派生字段 `plugin_origins`）；
- `OPENCODE_DISABLE_PROJECT_CONFIG` 同时跳过第 4 层与项目内的 `.opencode`；
- 第 5 层的顺序容易踩坑：`ConfigPaths.directories()`（`paths.ts:23`）向上收集 `.opencode` 时「近者在前」，却没有像 `files()` 那样 `toReversed()`，所以多级 `.opencode` 并存时外层反而覆盖内层。新内核扳正了这一点（见文末）。

## 合并之后的修正

`config.ts:550-598` 还做了几步语义修正：废弃的 `mode` 并入 `agent` 并强制为 primary；`OPENCODE_PERMISSION` 深合并进 `permission`；旧的 `tools` 开关表翻译成权限，`write`、`edit`、`patch` 折叠为 `edit`，显式的 `permission` 优先；`autoshare: true` 等价于 `share: "auto"`。V2 设计文档称这种翻译为“lossy compatibility input”，决定不再保留。

## JSONC：宽进严出

`ConfigParse.jsonc()` 基于 `jsonc-parser`，允许注释与尾逗号，报错带行号、列号和指向出错处的 `^`。`ConfigParse.schema()` 解码时用 `errors: "all"` 与 `onExcessProperty: "ignore"`：**不认识的字段静默忽略，已知字段写错类型直接报错**。内置 skill 的注释说得直白——“opencode hard-fails on invalid config”。

## 变量替换：先换文本，再做解析

`ConfigVariable.substitute()` 在 JSONC 解析**之前**处理原始文本：

```ts
/** Apply {env:VAR} and {file:path} substitutions to config text. */
export async function substitute(input: SubstituteInput) {
  const missing = input.missing ?? "error"
  let text = input.text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
    return (input.env?.[varName] ?? process.env[varName]) || ""
  })
```

（`packages/opencode/src/config/variable.ts:33-38`）

- `{env:VAR}` 缺失时替换为空串，不报错；
- `{file:path}` 相对**声明它的配置文件所在目录**解析，支持 `~/`，内容 `trim()` 后按 JSON 字符串转义嵌入；文件不存在报 `bad file reference`（TUI 配置改为空串）；整行 `//` 注释里的 `{file:...}` 会被跳过。

```jsonc
{
  "$schema": "https://opencode.ai/config.json",
  "instructions": ["CONTRIBUTING.md"],
  "permission": { "bash": { "*": "ask", "git status": "allow" } },
  "mcp": {
    "docs": {
      "type": "remote",
      "url": "https://docs.example.com/mcp",
      "headers": { "Authorization": "Bearer {env:DOCS_TOKEN}" }
    }
  }
}
```

## `$schema` 与 JSON Schema

`$schema` 指向的 `https://opencode.ai/config.json` 由 `packages/opencode/script/schema.ts` 从 `ConfigV1.Info` 生成，附加 `allowComments` 与 `allowTrailingCommas`；`tui.json` 的 schema 由 `TuiConfig.Info` 生成。加载器还会主动维护它：全局配置不存在时先写一个只含 `$schema` 的文件，读到缺 `$schema` 的配置就改写原文件补上（`config.ts:245-249`）。V2 设计文档的态度相反：“loading config must not insert it or create files for it”。

## Markdown 定义的 agent、命令与更多

每个配置目录（全局目录、各级 `.opencode/`、`~/.opencode/`、`OPENCODE_CONFIG_DIR`）还会按约定子目录扫描：

| 模式 | 产物 |
| --- | --- |
| `{agent,agents}/**/*.md` | agent，frontmatter 是字段，正文是 `prompt` |
| `{mode,modes}/*.md` | 旧式 mode，强制为 primary agent |
| `{command,commands}/**/*.md` | 命令，正文是 `template` |
| `{skill,skills}/**/SKILL.md` | skill |
| `{plugin,plugins}/*.{ts,js}` 与 `{tool,tools}/*.{js,ts}` | 本地插件与自定义工具 |

名字取自相对路径，`agents/review/security.md` 得到 `review/security`。frontmatter 解析失败时还有一次宽松重试（`packages/core/src/config/markdown.ts:20-21`）：

> Other coding agents accept unquoted colons in frontmatter values. Retry those values as YAML block scalars so existing config files keep working.

opencode 包里的同名文件点名了 Claude Code。加载器还会在每个配置目录后台安装匹配版本的 `@opencode-ai/plugin` 并写好 `.gitignore`，让本地插件能直接 `import` 到 API。

## TUI 配置单独成文件

终端界面相关设置拆进了 `tui.json`（`packages/opencode/src/config/tui.ts`），顺序与服务端平行：全局 `tui.json(c)` → `OPENCODE_TUI_CONFIG` → 项目各级 `tui.json(c)` → `.opencode` 目录；字段有 `theme`、`keybinds`、`plugin`、`plugin_enabled`、`attention`、`diff_style`、`mouse` 等。

容错策略与服务端相反：“every broken-config path degrades gracefully rather than crashing TUI startup”，坏文件记警告后跳过。老版本写在 `opencode.json` 里的 `theme`、`keybinds`、`tui` 由 `tui-migrate.ts` 按目录迁移到 `tui.json`，原文件先备份为 `<原文件名>.tui-migration.bak`。

## 生效时机：失效与重建

配置按实例（目录）缓存在 `InstanceState` 中，全局配置另有一份永不过期的缓存。现役代码里找不到监视配置文件的逻辑，改动靠「清缓存 + 丢弃实例」生效：通过 HTTP 接口改全局配置（`updateGlobal`，`.jsonc` 以补丁方式写回）后丢弃所有实例；改项目配置（`update`）则等响应发出后丢弃当前实例；TUI 进程收到 `SIGUSR2` 时让 worker 执行 `reload`。下一次请求到来时按新配置重建。`opencode debug config` 可以打印合并后的最终结果。

## 通往 V2：兼容层与新内核

V2 设计（`specs/v2/config.md`）逐组评审了 V1 字段：集合改复数（`agents`、`permissions`、`plugins`、`providers`），`command` 移除、改由 skills 承担，MCP 服务器收进 `mcp.servers` 并以 `disabled` 取代 `enabled`，权限改为有序的 `{ action, resource, effect }` 规则数组。

**兼容层** `packages/opencode/src/config/v2-compat.ts` 让 V1 读懂一部分 V2 写法：`ConfigV2Compat.lower()` 在校验前把 `agents`、`commands`、`mcp.servers` 等降级为 V1 字段，产出 `invalid`、`unsupported`、`conflict`（新旧并存时保留旧值）三类诊断；唯独 V2 的 `permissions` 直接报错，提示 `run opencode2`。

**新内核** `packages/core/src/config.ts` 只认 `opencode.json(c)`，只在打开位置时读一次，结果是按优先级排好的 `Document` / `Directory` 列表，而非合并好的大表。`.opencode` 的顺序在这里被显式扳正：

```ts
    const directories = [
      globalDirectory,
      ...discovered
        .filter((item) => path.basename(item) === ".opencode")
        .toReversed()
        .map((directory) => AbsolutePath.make(directory)),
    ]
    // A config closer to the opened directory should win over one higher up.
    // Search starts nearby, so reverse the results before applying them.
    const directPaths = discovered.filter((item) => path.basename(item) !== ".opencode").toReversed()
```

（`packages/core/src/config.ts:186-195`）

下一篇：[Agent、命令与 Skills](https://daiw.org/manual/opencode/agents-and-commands)——七个内置 agent 的权限设计、命令模板的展开顺序与 SKILL.md 的发现规则。
