Skip to content

openclaw.json 配置入口

源码版本v2026.6.11

职责

openclaw.json 是 OpenClaw 的唯一作者态配置 (config) 文件:gateway 端口、模型 provider、渠道凭据、技能加载路径、cron、记忆策略、approvals、hooks 全都写在这一个 JSON5 文件里。运行时一切配置都从这里读出来,再经过 include 展开、env 变量替换、Zod schema 校验、默认值注入,变成内存里的 OpenClawConfig 对象。

它不只是「读一次就完事」的文件:CLI 的 config set/patch/unset 会原子写回,运行时快照 (runtime snapshot) 会和磁盘内容比对防止竞态,出问题时还能从 last-known-good 备份回滚。所以这个入口承担读、写、校验、备份、迁移五件事。

设计动机

为什么是单文件而不是分散目录?早期 OpenClaw 把 provider、channel、memory 各自塞进独立 JSON,结果是用户改一个 provider 要改三个文件,渠道插件升级后又得把迁移逻辑撒得到处都是。收敛成单个 openclaw.json 之后,版本戳、备份、审计日志都挂在同一份文件上,openclaw doctor 扫描一个路径就能体检。

但单文件又会膨胀,所以同时支持两层逃生:

  • $include 指令把大段 provider/channel 配置拆到子文件,根文件只保留引用;
  • ${VAR} 环境变量替换让密钥脱离文件入库。

这两个特性让 openclaw.json 既能进 git 仓库(脱敏后),又能跑在 Docker/Tailscale 这类只给环境变量的部署里。

关键文件

数据流

启动时,createConfigIO 先把路径常量 pin 住,再走 loadConfigLocal(loadConfigLocal:1654)。读路径的关键几步:

typescript
function loadConfigLocal(options: { skipSuspiciousRecovery?: boolean } = {}): OpenClawConfig {
  try {
    maybeLoadDotEnvForConfig(deps.env);
    const envBeforeRead = snapshotEnv(deps.env);
    if (!deps.fs.existsSync(configPath)) {
      // ... 加载 shell env fallback 后返回空配置
      return {};
    }
    const raw = deps.fs.readFileSync(configPath, "utf-8");
    const parsed = deps.json5.parse(raw);
    const readResolution = resolveConfigForRead(
      resolveConfigIncludesForRead(parsed, configPath, deps),
      deps.env,
      deps.lowerPrecedenceEnv,
    );
    // ... shipped plugin install records 迁移、validate、materialize

注意三件事:maybeLoadDotEnvForConfig 只在 env 等于 process.env 时才真正加载 dotenv(测试注入的 env 不动);envBeforeRead 用来在写回时检测「读期间 env 是否被改」;resolveConfigIncludesForRead 会把 $include 目标文件读进来,然后再交给 resolveConfigForRead${VAR} 替换。顺序很关键:include 先于 env 替换,这样 include 进来的字段里也能用 ${VAR}

写路径走 writeConfigFile(writeConfigFile:2826)。当存在 runtime snapshot 时,它先把 caller 给的 runtime 形配置 diff 成 patch,再投影回 source 形态:

typescript
export async function writeConfigFile(
  cfg: OpenClawConfig,
  options: ConfigWriteOptions = {},
): Promise<ConfigWriteResult> {
  options.assertConfigPathForWrite?.();
  const io = createConfigIO({ /* ... */ });
  assertConfigWriteAllowedInCurrentMode({ configPath: io.configPath });
  let nextCfg = cfg;
  const runtimeConfigSnapshot = getRuntimeConfigSnapshotState();
  const runtimeConfigSourceSnapshot = getRuntimeConfigSourceSnapshotState();
  const hadBothSnapshots = Boolean(runtimeConfigSnapshot && runtimeConfigSourceSnapshot);
  if (hadBothSnapshots) {
    const runtimePatch = createMergePatch(runtimeConfigSnapshot!, cfg);
    nextCfg = coerceConfig(applyMergePatch(runtimeConfigSourceSnapshot!, runtimePatch));
  }

这一步保证了 caller 写回的是「用户作者态」,而不是被运行时默认值污染过的形态——否则 schema 默认值会被固化进文件,改版本时回不来。

CLI patch 模式(runConfigPatch:2279)接收一个 JSON5 patch 对象,递归合并进现有配置:

typescript
function mergeConfigValue(existing: unknown, patch: unknown, path: PathSegment[]): unknown {
  if (isProviderModelListPath(path) && Array.isArray(existing) && Array.isArray(patch)) {
    return mergeModelArrays(existing, patch);
  }
  if (isPlainRecord(existing) && isPlainRecord(patch)) {
    // 深合并:对象递归,非对象直接覆盖

models[] 这种数组走 mergeModelArrays,按 id 字段去重后合并元素,而不是简单替换——provider 列表里追加一个 model 不会把原有 model 全抹掉。

整体读、写、patch 三条路径的关系:

边界与失败

  • 路径漂移:gateway 启动时必须先调 pinRuntimePaths(pinRuntimePaths:236-245),否则 OPENCLAW_CONFIG_PATH 在 module import 之后才设置会读到旧值,出现「读一个路径、写另一个路径」的撕裂。
  • legacy 回退:resolveConfigPath 先扫 clawdbot.json.clawdbot/,没有再走新路径。升级老部署时若两个目录都存在,会优先新路径;但用户若误把旧文件留在 ~/.openclaw/,新进程也会读它——doctor 命令专门扫这种分裂。
  • $include 越界:默认只能引用 openclaw.json 同目录及子目录文件。要跨目录必须显式设 $OPENCLAW_INCLUDE_ROOTS,且每个根会被 tilde 展开 + 去重 + 拒绝非绝对路径(include roots filter:128-141)。
  • ${VAR} 缺失:不抛错,只 warn「feature using this value will be unavailable」(src/config/io.ts:1950-1956),gateway 进入降级模式而非拒绝启动。但 provider apiKey 缺失会在真正发请求时报错。
  • Nix 模式只读:OPENCLAW_NIX_MODE=1 时,assertConfigWriteAllowedInCurrentMode 会拒绝所有写操作,配置由 Nix 外部托管(resolveIsNixMode:16-18)。
  • 原子写:replaceFileAtomic 写临时文件再 rename,目录权限 0o700、文件权限 0o600,Windows 走 copy fallback。
  • 大小骤降拦截:写之前对比 previousBytes vs nextBytes,若降到 50% 以下且无 allowConfigSizeDrop 旗标,直接拒绝写——防止某次 patch 把 provider 整块删掉。
  • last-known-good:每次成功读会更新 lastKnownGood 指纹(hash + bytes + mtime + dev/ino);若下次读发现 size 暴跌或 gateway mode 字段被抹,自动从 .bak 恢复,审计日志记一行 config.observe 事件。
  • future version 警告:文件 meta.lastTouchedVersion 比当前二进制新时 warn,提示用户 PATH 可能指向旧版 openclaw——避免「新版写出的配置被旧版读坏」。

小结

openclaw.json 是单一入口,但背后是一整套读-校验-物化-写-审计的生命周期。读路径层层展开 include 和 env,写路径把 runtime 形态反向投影回 source 形态,patch 模式给 CLI 提供结构化增量编辑。schema 怎么定义每个字段的合法形状,看 Zod Schema 运行时校验;provider 的具体字段(baseUrl/api/auth/apiKey/models[])在 能力层:模型 Provider 详述;渠道侧配置看 渠道:配置;常驻进程怎么把读到的 config 用起来看 部署:daemon

对照官方资料:Configuration 文档 · README