openclaw.json-Konfigurationseingang
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:
$includelagert 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
paths.ts CONFIG_FILENAME:25-26—openclaw.json-Dateinamenkonstante; daneben gibt es noch ein Legacy-clawdbot.json-Fallback.resolveStateDir:58-87— Löst das Zustandsverzeichnis (state dir) auf;$OPENCLAW_STATE_DIRüberschreibt, sonst~/.openclaw, sonst Legacy.clawdbot.resolveCanonicalConfigPath:152-161— Kanonischer Pfad$OPENCLAW_CONFIG_PATHoder$stateDir/openclaw.json.resolveConfigPath:191-226— Beim tatsächlichen Lesen wird zuerst eine existierende Kandidatendatei (inkl. Legacy) gesucht; erst danach fällt es auf canonical zurück.normalizeStateDirEnv:89-95— Baut beim Start~in$OPENCLAW_STATE_DIRaus und schreibt es in env zurück; erste Aktion der Gateway-Startsequenz.resolveIncludeRoots:117-143—$OPENCLAW_INCLUDE_ROOTS-Whitelist; erlaubt$includeüber das Konfigurationsverzeichnis hinauszugehen.createConfigIO:1378-1434— IO-Fassaden-Factory; kapselt Lesen, Schreiben, Snapshot, Backup, Validierung in einem Fluss.loadConfigLocal:1654-1828— Synchroner Lesepfad-Kern: dotenv → Datei lesen → JSON5 parse → include ausrollen → env ersetzen → validieren → Laufzeitkonfiguration materialisieren.writeConfigFileLocal:2269-2366— Schreibpfad-Kern: Snapshot vergleichen, merge-patch erzeugen, validieren, atomar auf Platte schreiben.writeConfigFile Export:2826-2860— Öffentlicher Schreib-Eintritt; projiziert Runtime-Snapshot zurück in Source-Form und schreibt.applyMergePatch:83— JSON merge patch-Implementierung, Basis des patch-Modus.applyConfigOverrides:93— Laufzeit-Injektion von Überschreibungen (Tests, CLI-Temp-Parameter).env-substitution.ts—${VAR}-Auflösung; fehlt die Variable, nur warn, nicht crash.includes.ts—$include-Direktiven-Auflösung mit Pfad-Escape-Schutz.defaults.ts apply*:109-529—applyMessageDefaults/applyAgentDefaults/applyCronDefaults/applyCompactionDefaultsu. a. Default-Injektionen.runConfigPatch:2279-2309—openclaw config patchCLI-Eintritt.mergeConfigValue:771-790— Rekursive Verschmelzung beim patch; fürmodels[]-Array des Providers Spezial-Entdopplungspfad.
Datenfluss
Beim Start pinnt createConfigIO zuerst die Pfadkonstanten und geht dann durch loadConfigLocal (loadConfigLocal:1654). Die zentralen Schritte des Lesepfads:
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, materializeDrei 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:
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:
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 überschreibenArrays 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, wennOPENCLAW_CONFIG_PATHnach Module-Import gesetzt wird, einen veralteten Wert — das erzeugt den Riss „einen Pfad lesen, einen anderen schreiben". - Legacy-Fallback:
resolveConfigPathscannt zuerstclawdbot.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 vonopenclaw.jsonverwiesen werden. Will man verzeichnisübergreifend referenzieren, muss$OPENCLAW_INCLUDE_ROOTSexplizit 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=1lehntassertConfigWriteAllowedInCurrentModejeden Schreib ab; die Konfiguration wird von Nix extern verwaltet (resolveIsNixMode:16-18). - Atomares Schreiben:
replaceFileAtomicschreibt eine temporäre Datei und benennt um; Verzeichnisrechte0o700, Dateirechte0o600, Windows geht über copy-Fallback. - Größen-Einbruch-Abwehr: Vor dem Schreiben wird
previousBytesvs.nextBytesverglichen; fällt die Größe unter 50 % und ist keinallowConfigSizeDrop-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.bakwiederhergestellt und das Audit-Log schreibt einconfig.observe-Ereignis. - future-version-Warnung: Ist
meta.lastTouchedVersionder 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.