Skip to content

openclaw.json-Konfigurationseingang

源码版本v2026.6.11

Verantwortung

openclaw.json ist die einzige Autor-Zustand-Konfiguration (config) von OpenClaw: Gateway-Port, Model-Provider, Kanal-Anmeldedaten, Fertigkeiten-Ladepfade, Cron, Gedächtnis-Strategie, Approvals, Hooks — alles in einer JSON5-Datei. Zur Laufzeit wird jede Konfiguration von hier gelesen, dann über Include-Ausrollung, Env-Variablen-Ersetzung, Zod-Schema-Validierung und Default-Wert-Injektion zum OpenClawConfig-Objekt im Speicher.

Es ist keine „einmal lesen, fertig"-Datei: config set/patch/unset der CLI schreibt atomar zurück; ein Laufzeit-Snapshot (runtime snapshot) wird mit dem Inhalt der Platte verglichen, um Race-Conditions zu verhindern; im Fehlerfall lässt sich aus einer last-known-good-Sicherung zurückrollen. Dieser Eingang trägt also fünf Dinge: Lesen, Schreiben, Validieren, Backup, Migration.

Designmotivation

Warum eine Einzeldatei statt verteilter Verzeichnisse? Früh in OpenClaw steckte provider/channel/memory je in einer eigenen JSON; das führte dazu, dass das Ändern eines Providers drei Dateien berührte und nach Plugin-Upgrades die Migrationslogik überall verstreut war. Nach der Konzentration auf eine einzige openclaw.json hängen Versionsstempel, Backup, Audit-Log alle an derselben Datei; openclaw doctor kann mit einem Pfad-Scan den Gesundheits-Check machen.

Aber eine Einzeldatei bläht auf, deshalb gibt es zwei Escapes:

  • $include lagert große Provider-/Channel-Konfigurationsblöcke in Unterdateien aus; die Wurzeldatei behält nur die Referenz.
  • ${VAR} Umgebungsvariablen-Ersetzung nimmt Secrets aus der Datei heraus.

Diese zwei Eigenschaften lassen openclaw.json nach Maskierung in ein Git-Repo und zugleich in Docker-/Tailscale-Deployments laufen, die nur Umgebungsvariablen mitgeben.

Schlüsseldateien

Datenfluss

Beim Start pinnt createConfigIO zuerst die Pfadkonstanten und geht dann durch loadConfigLocal (loadConfigLocal:1654). Die zentralen Schritte des Lesepfads:

typescript
function loadConfigLocal(options: { skipSuspiciousRecovery?: boolean } = {}): OpenClawConfig {
  try {
    maybeLoadDotEnvForConfig(deps.env);
    const envBeforeRead = snapshotEnv(deps.env);
    if (!deps.fs.existsSync(configPath)) {
      // ... lädt shell env fallback und leere Konfiguration zurück
      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 Migration, validate, materialize

Drei Dinge zu beachten: maybeLoadDotEnvForConfig lädt dotenv nur, wenn env identisch mit process.env ist (injizierte Test-envs werden nicht angerührt); envBeforeRead dient beim Zurückschreiben der Prüfung, ob env während des Lesens geändert wurde; resolveConfigIncludesForRead liest $include-Zieldateien ein, bevor resolveConfigForRead ${VAR} ersetzt. Die Reihenfolge ist kritisch: include vor env, damit inkludierte Felder ebenfalls ${VAR} nutzen können.

Der Schreibpfad läuft über writeConfigFile (writeConfigFile:2826). Liegt ein Runtime-Snapshot vor, diff't es die vom Caller gegebene Runtime-Form zum patch und projiziert zurück in die Source-Form:

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));
  }

Dieser Schritt sichert, dass der Caller die „Nutzer-Autor-Form" zurückschreibt, nicht die mit Runtime-Defaults verschmutzte Form — sonst würden Schema-Defaults in die Datei gebacken und bei Versionswechseln nicht mehr zurückholbar.

Der CLI-patch-Modus (runConfigPatch:2279) nimmt ein JSON5-Patch-Objekt und verschmilzt rekursiv in die bestehende Konfiguration:

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)) {
    // Tief verschmelzen: Objekt rekursiv, Nicht-Objekt direkt überschreiben

Arrays wie models[] gehen durch mergeModelArrays und werden nach id-Feld entdopplt zusammengeführt, statt einfach ersetzt zu werden — ein neues Model im Provider wischt vorhandene Modelle nicht weg.

Das Verhältnis der drei Pfade Lesen / Schreiben / patch:

Grenzen und Fehler

  • Pfad-Drift: Beim Gateway-Start muss zuerst pinRuntimePaths (pinRuntimePaths:236-245) gerufen werden, sonst liest man, wenn OPENCLAW_CONFIG_PATH nach Module-Import gesetzt wird, einen veralteten Wert — das erzeugt den Riss „einen Pfad lesen, einen anderen schreiben".
  • Legacy-Fallback: resolveConfigPath scannt zuerst clawdbot.json, .clawdbot/; fehlt beides, geht es auf den neuen Pfad. Beim Upgrade einer Alt-Deployment mit beiden Verzeichnissen gewinnt der neue Pfad; lässt ein Nutzer die alte Datei versehentlich in ~/.openclaw/ stehen, liest der neue Prozess sie — der doctor-Befehl scannt genau diese Spaltung.
  • $include-Escape: Standardmäßig darf nur auf Dateien innerhalb desselben Verzeichnisses oder Unterverzeichnisse von openclaw.json verwiesen werden. Will man verzeichnisübergreifend referenzieren, muss $OPENCLAW_INCLUDE_ROOTS explizit gesetzt sein; jeder Root wird tilde-ausgerollt + entdopplt + nicht-absolute Pfade abgewiesen (include roots filter:128-141).
  • ${VAR} fehlt: kein Throw, nur warn „feature using this value will be unavailable" (src/config/io.ts:1950-1956), das Gateway geht in den Degradationsmodus statt den Start zu verweigern. Fehlt jedoch eine Provider-apiKey, schlägt es erst beim tatsächlichen Request fehl.
  • Nix-Modus read-only: Bei OPENCLAW_NIX_MODE=1 lehnt assertConfigWriteAllowedInCurrentMode jeden Schreib ab; die Konfiguration wird von Nix extern verwaltet (resolveIsNixMode:16-18).
  • Atomares Schreiben: replaceFileAtomic schreibt eine temporäre Datei und benennt um; Verzeichnisrechte 0o700, Dateirechte 0o600, Windows geht über copy-Fallback.
  • Größen-Einbruch-Abwehr: Vor dem Schreiben wird previousBytes vs. nextBytes verglichen; fällt die Größe unter 50 % und ist kein allowConfigSizeDrop-Flag gesetzt, wird der Schreib abgewiesen — verhindert, dass ein patch versehentlich einen ganzen Provider-Block löscht.
  • last-known-good: Jeder erfolgreiche Lese-Vorgang aktualisiert den lastKnownGood-Fingerabdruck (hash + bytes + mtime + dev/ino); fällt beim nächsten Lesen die Größe drastisch oder wird das gateway-mode-Feld gelöscht, wird automatisch aus .bak wiederhergestellt und das Audit-Log schreibt ein config.observe-Ereignis.
  • future-version-Warnung: Ist meta.lastTouchedVersion der Datei neuer als die laufende Binärdatei, wird gewarnt, dass der PATH womöglich auf eine alte openclaw-Version zeigt — verhindert „neue Version schreibt Konfig, alte Version liest sie kaputt".

Zusammenfassung

openclaw.json ist ein einzelner Eingang, trägt aber einen ganzen Lebenszyklus aus Lesen-Validieren-Materialisieren-Schreiben-Auditieren. Der Lesepfad rollt include und env schrittweise aus; der Schreibpfad projiziert die Runtime-Form zurück in die Source-Form; der patch-Modus gibt der CLI strukturierte inkrementelle Bearbeitung. Wie das Schema die gültige Form jedes Feldes definiert, siehe Zod-Schema-Laufzeitvalidierung; die spezifischen Provider-Felder (baseUrl/api/auth/apiKey/models[]) werden unter Fähigkeiten: Provider beschrieben; Kanal-Konfiguration siehe Kanal-Konfiguration; wie der Daemon die gelesene config nutzt, siehe Deployment: Daemon.

Vergleich mit offiziellen Ressourcen: Configuration-Doku · README.