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 三條路徑的關係:

邊界與失敗

  • 路徑漂移:閘道啟動時必須先調 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),閘道進入降級模式而非拒絕啟動。但 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