## What Problem This Solves Fixes root config write regressions introduced by #145983: failed config saves could remove the root and recommend a destructive retry, or discard recovery backups before staging succeeded. ## Why This Change Was Made Root and included-file saves share staged publication and safe restoration. Backup bytes are prepared asynchronously; rotation follows successful staging. Partial failures report the original error, recovery status, and backup location. The writer rechecks captured include path identities during publication and restoration. Backup permissions change only through admitted regular-file descriptors. The decision `which backup files may be mutated` is made by exactly one mechanism at `src/config/backup-rotation.ts:78`. The decision `whether captured include paths are unchanged` is made by exactly one mechanism at `src/config/io.write-safety.ts:80`. ## User Impact Failed staging preserves all five recovery generations. Interrupted saves restore the original file when safe. Gateway clients receive complete recovery details. Cancelled login and setup writes retain the cancellation reason. ## Evidence Before: permission-denied fallback plus a concurrent include edit removed the root; following retry advice lost Gateway settings and cold startup failed. Five disk-full saves consumed the backup history. The same CLI scenarios now restore the root, preserve all five root and include backups, and retain Gateway settings on the next save. Authenticated cold health passes; a wrong token is rejected. All three Gateway save methods return complete structured recovery failures. Aliased includes survive denied rename, validate, and cold-start; linked backup targets retain their bytes and permissions. Detailed consumer coverage is retained in `proof/consumers.md`. ## Compatibility No config keys, stored formats, protocol schema, or protocol version change. Partial publication failures use the existing `UNAVAILABLE` outcome. Plain-root legacy mutation adapters retain their supplied revision; include-bearing adapters without a committed snapshot use the existing unknown revision and can refresh before their next edit. ## Consumers - Config CLI saves preserve root and include recovery backups, including through symlinked includes, and report restoration outcomes. - Gateway `config.set`, `config.patch`, and `config.apply` preserve complete recovery messages and structured details. - Login and service-install writers check live authority immediately before file effects. - Deferred chat and Telegram native login consent recheck retained flow and caller authority at publication. - Gateway setup activation carries cancellation through the shared config writer. - Legacy include mutation adapters receive an unknown revision when no committed composite revision is available. <details> <summary>Internal call migration</summary> The counts below refer to retired adapters and synthetic test-fixture fields. Filesystem options, the public authority callback, and real token/version contracts remain supported. census: generic assertConfigPathForWrite reviewed — 0 callers listed census: generic chmod reviewed — 0 callers listed census: generic copyFallbackOnPermissionError reviewed — 0 callers listed census: generic copyFile reviewed — 0 callers listed census: generic dirMode reviewed — 0 callers listed census: generic fileSystem reviewed — 0 callers listed census: generic maintainConfigBackups reviewed — 0 callers listed census: generic mkdir reviewed — 0 callers listed census: generic open reviewed — 0 callers listed census: generic overwrite reviewed — 0 callers listed census: generic promises reviewed — 0 callers listed census: generic rename reviewed — 0 callers listed census: generic tempPrefix reviewed — 0 callers listed census: generic to reviewed — 0 callers listed census: generic token reviewed — 0 callers listed census: generic unlink reviewed — 0 callers listed census: generic version reviewed — 0 callers listed </details> ## Invalidation Existing config watchers and runtime refresh remain unchanged. Captured path facts are scoped to one write and its restoration. Failed saves publish no success acknowledgement. ## Contention Backup preparation yields to unrelated Gateway requests. With a 2,000 ms injected preparation delay, an unrelated health request completed in 90 ms (limit: 1,000 ms). Candidate staging, backup renames, and final checks stay synchronous. ## Tests 374 targeted config-owner and CLI tests passed, including linked backup slots, alias fallback, alias replacement, and missing-parent repair. Registered `openclaw config set`, `config.set`, `config.patch`, `config.apply`, `models.authLogin`, and `wizard.next` cover config writes. Provider login is synthetic. Service-token/install tests use a mocked writer and prove forwarding and refusal, not native service effects. Package-entry consumers cover legacy revision compatibility. CI hits the same `server-startup-finish.ts:416` type error as [main run 34718269980](https://github.com/openclaw/openclaw/actions/runs/34718269980), introduced by #146275. The config tests, formatting, line limits, assertions, and unused-export checks pass. Follow-up: Settings reload/save after a missing root can create defaults; that existing UI behavior is tracked separately. Co-authored-by: Ayaan Zaidi <hi@obviy.us>
30 KiB
| summary | read_when | title | sidebarTitle | ||
|---|---|---|---|---|---|
| CLI reference for `openclaw config` (get/set/patch/unset/file/schema/validate) |
|
Config | Config |
Non-interactive helpers for openclaw.json: get/set/patch/unset a value by path, print the schema, validate, or print the active file path. Run openclaw config with no subcommand to open the same guided wizard as openclaw configure.
Externally managed config
Set OPENCLAW_CONFIG_READONLY=1 in the environment of both the Gateway and any
OpenClaw CLI processes when a deployment system manages your config:
export OPENCLAW_CONFIG_READONLY=1
openclaw config validate
openclaw gateway run
For a service or container, set the variable in its service environment or
container definition. This is a host-environment switch, not an openclaw.json
field. Do not set OPENCLAW_CONFIG_READONLY in config env or env.vars.
Entries in env.vars are ignored, including differently cased spellings; flat
env keys are not valid configuration. Config reload cannot enable, disable,
or change the host-selected read-only mode. Only the host value 1 enables
this switch. Existing OPENCLAW_NIX_MODE behavior is unchanged.
Config writes are blocked, including setup, onboarding, doctor repairs, plugin
install/update/uninstall/enable/disable, and mutating openclaw update flows.
Startup-derived defaults stay runtime-only. Change the config through your
external deployment system, then let the Gateway reload it or restart the Gateway
as needed. Runtime state still needs a writable OPENCLAW_STATE_DIR.
OPENCLAW_CONFIG_READONLY=1 uses generic externally managed config messages and
does not enable Nix-specific installation or service behavior. OPENCLAW_NIX_MODE=1
continues to imply immutable config, even if OPENCLAW_CONFIG_READONLY is unset or
0. For Nix installs, edit the Nix source instead; see Nix.
Root options
Repeatable guided-setup section filter when you run `openclaw config` without a subcommand.Guided sections: workspace, model, web, gateway, daemon, channels, plugins, skills, health.
Examples
openclaw config file
openclaw config file --json
openclaw config --section model
openclaw config --section gateway --section daemon
openclaw config schema
openclaw config schema --json
openclaw config get browser.executablePath
openclaw config set browser.executablePath "/usr/bin/google-chrome"
openclaw config set browser.profiles.work '{"cdpPort":18801,"executablePath":"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"}' --strict-json --merge
openclaw config set agents.defaults.heartbeat.every "2h"
openclaw config set logging.audit.executionIdentity true
openclaw config set 'agents.entries.main.tools.exec.node' "node-id-or-name"
openclaw config set agents.defaults.models '{"openai/gpt-5.4":{}}' --strict-json --merge
openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN
openclaw config set secrets.providers.vaultfile --provider-source file --provider-path /etc/openclaw/secrets.json --provider-mode json
openclaw config patch --file ./openclaw.patch.json5 --dry-run
openclaw config unset plugins.entries.brave.config.webSearch.apiKey
openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN --dry-run
openclaw config validate
openclaw config validate --json
Paths
Dot or bracket notation. Quote bracket paths in shell examples so zsh does not glob-expand [0]:
openclaw config get agents.defaults.workspace
openclaw config get agents.entries.main
openclaw config get agents.entries
openclaw config set 'agents.entries.work.tools.exec.node' "node-id-or-name"
Prefer agents.entries.<id> paths for agent edits. The legacy agents.list[0]
syntax and whole-list inputs still work with set, patch, and unset; writes
persist the canonical keyed roster. Indexed edits use the current roster order.
Within a batch, a submitted list keeps its order across subsequent keyed edits,
including when agent IDs are numeric strings. Existing roster-deletion and
$include ownership protections still apply.
When a legacy roster expands a single-agent installation in the root config file,
writes retire its default marker and preserve the existing agent's responsibilities
with explicit owners. An explicitly authored ownership: "explicit" cannot be
combined with a legacy default: true marker.
For root-file writes, changing session.store clears a copied
agents.defaults.sessionStore.agentId because that owner belongs to the previous
store. To assign the destination store's owner, set that owner path explicitly in
the same batch.
config get
Reads a value from the redacted config snapshot (secrets never print). --json prints the same redacted value as JSON; otherwise strings/numbers/booleans print bare and objects/arrays print as formatted JSON.
Pass exactly one config path. Extra arguments, including an empty quoted argument (""),
are rejected; they do not suppress validation of later options.
A schema-valid but unset path explains that the runtime default applies; an unknown path suggests
openclaw config schema. With --json, both use the standard CLI JSON failure envelope
on stdout and exit with status 1. Without --json, diagnostics remain on stderr.
Explicit null, false, 0, and empty strings remain readable values in both modes;
--json preserves their types. Optional fields with no runtime value are reported as unset.
openclaw config get browser.executablePath
openclaw config get agents.defaults.model --json
config file
Prints the active config file path, resolved from OPENCLAW_CONFIG_PATH or the default location. The path names a regular file, not a symlink; see Write safety.
With --json, stdout contains an object with the resolved path under path.
config schema
Prints the generated JSON schema for openclaw.json to stdout.
openclaw config schema
openclaw config schema --json
openclaw config schema > openclaw.schema.json
The schema is JSON in both modes. --json is accepted as the explicit
machine-output spelling and keeps stdout reserved for the schema document.
config validate
Human validation diagnostics quote literal record keys, such as agents.defaults.models["provider/model.v1"].alias, instead of displaying the dot inside a key as nested traversal. Numeric array positions use brackets, such as agents.entries.main.skills[0]. The issues[].path field in config validate --json keeps its existing dot-joined representation.
Validates the current config against the active schema without starting the gateway. It also checks provider/source compatibility for every registry-declared SecretRef, including disabled plugin or channel configuration. This strict command can report an inactive mismatch that does not block normal Gateway startup, where SecretRef resolution remains limited to effectively active surfaces.
After schema validation, it checks every configured manual exec provider's command path using the same non-executing trust checks as startup: file presence, symlinks, trusted directories, permissions, ownership, and Windows ACL availability. config set, config patch, and config unset apply these checks only to providers changed or referenced by the operation, including during dry runs. Replacing the secrets or secrets.providers collection checks every remaining provider. An unrelated inactive provider does not block targeted repairs or removal of that provider.
Path validation does not execute providers or verify their output. Passing it does not guarantee successful secret resolution; exec dry runs require --allow-exec to test that separately.
openclaw config validate
openclaw config validate --json
The exec-provider checks inspect the filesystem of the host where the CLI
runs. Run `config validate` on the gateway host itself (or on a host with
matching command paths, ownership, and ACLs). Paths and permissions can change
after validation; startup checks them again before execution.
If validation is already failing, start with `openclaw configure` or `openclaw doctor --fix`. `openclaw chat` does not bypass the invalid-config guard.
Provider and runtime params bags are intentionally typed as
Record<string, unknown> because their owners define the supported keys and
values. openclaw config validate can validate the container and overall
config shape, but it cannot type-check provider-specific parameter names or
values. Passing validation does not prove that a param is supported; consult
the provider docs and verify behavior on the selected runtime and provider.
Values
Values parse as JSON5 when possible; otherwise they are treated as raw strings. Use --strict-json to require standard JSON with no string fallback (JSON5-only syntax such as comments, trailing commas, or unquoted keys is then rejected). --json is a legacy alias for --strict-json on config set.
openclaw config set agents.defaults.heartbeat.every "0m"
openclaw config set gateway.port 19001 --strict-json
openclaw config set channels.whatsapp.groups '{"*":{"requireMention":true}}' --strict-json
For structured values that are awkward to quote in your shell, put a config-shaped JSON5 object in a file and use config patch --file <path> --dry-run. The file contains config keys and their values, not a bare array.
config get <path> --json prints the redacted value as JSON instead of terminal-formatted text.
When a write changes agents.defaults.model or a per-agent agents.entries.*.model, OpenClaw resolves each changed primary or fallback through the configured catalogs and the selected provider's model resolver before writing. Provider-supported exact provider/model pins are accepted even when absent from the curated picker; validation does not replace the selected model. Unknown model references are rejected without changing the active config. Run openclaw models list to browse the picker, or check the provider's documentation for an exact model ID. Successful validation does not prove that your account can call the model. openclaw models set is deliberately more permissive for the same setting: it saves a model the local catalog cannot confirm and prints a warning instead of rejecting the write.
Use --merge when adding entries to those maps:
openclaw config set agents.defaults.models '{"openai/gpt-5.4":{}}' --strict-json --merge
openclaw config set models.providers.ollama.models '[{"id":"llama3.2","name":"Llama 3.2"}]' --strict-json --merge
Use --replace only when the provided value should intentionally become the complete target value.
Conditional writes
Use a conditional expectation when automation must update one authored path only if it has not changed since the caller last observed it:
openclaw config set gateway.port 19001 --strict-json --expect-current-json 18789
openclaw config set gateway.port 19001 --strict-json --expect-current-absent
--expect-current-json <json> uses strict JSON and compares the value by JSON type and structure.
null is an authored value, so it does not satisfy --expect-current-absent. The comparison uses
the effective authored config after includes and environment substitution, before runtime defaults
are applied.
The two expectation flags are mutually exclusive. They apply only to a single config set
operation, require a direct non-redirected config path, and cannot be combined with batch mode or
--dry-run. If input or roster resolution would write a different path than the caller requested,
such as a sibling *Ref path, the command exits with status 1 instead of retargeting the
expectation. A mismatch exits with status 1, writes nothing, and does not print either the expected
or current value. OpenClaw's config snapshot guard still rejects a later race between the
expectation check and the final file replacement.
config set modes
```bash
openclaw config set
```
```bash
openclaw config set channels.discord.token \
--ref-provider default \
--ref-source env \
--ref-id DISCORD_BOT_TOKEN
```
Targets `secrets.providers.` paths only:
```bash
openclaw config set secrets.providers.vault \
--provider-source exec \
--provider-command /usr/local/bin/openclaw-vault \
--provider-arg read \
--provider-arg openai/api-key \
--provider-timeout-ms 5000
```
```bash
openclaw config set --batch-json '[
{
"path": "secrets.providers.default",
"provider": { "source": "env" }
},
{
"path": "channels.discord.token",
"ref": { "source": "env", "provider": "default", "id": "DISCORD_BOT_TOKEN" }
}
]'
```
```bash
openclaw config set --batch-file ./config-set.batch.json --dry-run
```
Batch files are limited to 8 MiB.
SecretRef assignments are rejected on unsupported runtime-mutable surfaces (for example `hooks.token`, Discord thread-binding webhook tokens, and WhatsApp creds JSON). See [SecretRef Credential Surface](/reference/secretref-credential-surface).
Batch parsing always uses the batch payload (--batch-json/--batch-file) as the source of truth; --strict-json / --json do not change batch parsing behavior.
Supplying either batch option selects batch mode. Empty or whitespace-only values are rejected; omit both options to use positional <path> <value> mode.
Batch assignments apply in order, then validation checks the final config. A SecretRef replaced by a later assignment is not resolved or counted in dry-run output, even with --allow-exec. Providers that remain in a changed provider collection still receive command-path trust checks.
JSON path/value mode also works for SecretRefs and providers directly:
openclaw config set channels.discord.token \
'{"source":"env","provider":"default","id":"DISCORD_BOT_TOKEN"}' \
--strict-json
openclaw config set secrets.providers.vaultfile \
'{"source":"file","path":"/etc/openclaw/secrets.json","mode":"json"}' \
--strict-json
Provider builder flags
Provider builder targets must use secrets.providers.<alias> as the path.
Hardened exec provider example:
openclaw config set secrets.providers.vault \
--provider-source exec \
--provider-command /usr/local/bin/openclaw-vault \
--provider-arg read \
--provider-arg openai/api-key \
--provider-json-only \
--provider-pass-env VAULT_TOKEN \
--provider-trusted-dir /usr/local/bin \
--provider-timeout-ms 5000
config patch
Paste or pipe a config-shaped JSON5 patch instead of running many path-based config set commands. Objects merge recursively; arrays and scalar values replace the target; null deletes the target path.
openclaw config patch --file ./openclaw.patch.json5 --dry-run
openclaw config patch --file ./openclaw.patch.json5
Patch files are limited to 8 MiB. Piped --stdin patches are limited to 1 MiB.
Pipe a patch over stdin for remote setup scripts:
ssh user@gateway-host 'openclaw config patch --stdin --dry-run' < ./openclaw.patch.json5
ssh user@gateway-host 'openclaw config patch --stdin' < ./openclaw.patch.json5
Example patch:
{
channels: {
slack: {
enabled: true,
mode: "socket",
botToken: { source: "env", provider: "default", id: "SLACK_BOT_TOKEN" },
appToken: { source: "env", provider: "default", id: "SLACK_APP_TOKEN" },
groupPolicy: "open",
requireMention: false,
},
discord: {
enabled: true,
token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
dmPolicy: "disabled",
dm: { enabled: false },
groupPolicy: "allowlist",
},
},
agents: {
defaults: {
model: { primary: "openai/gpt-6-astra" },
models: {
"openai/gpt-6-astra": {
agentRuntime: { id: "openclaw" },
params: { fastMode: true },
},
},
},
},
}
The runtime pin makes this an embedded OpenClaw recipe. A valid fastMode
value is a portable typed runtime control and does not choose OpenClaw by
itself.
Use --replace-path <path> when one object or array must become exactly the provided value instead of being recursively patched:
openclaw config patch --file ./discord.patch.json5 --replace-path 'channels.discord.guilds["123"].channels'
--dry-run runs schema and SecretRef resolvability checks without writing. Exec-backed SecretRefs are skipped by default during dry-run; add --allow-exec when you intentionally want dry-run to execute provider commands.
Dry run
--dry-run simulates a change without writing openclaw.json. Available on config set, config patch, and config unset. Which checks run depends on the input mode. Value mode (config set <path> <value> without --strict-json) skips the full schema pass and the ordinary SecretRef resolvability scan. Policy, provider, and model-reference checks can still run. When no checks apply, value mode reports Dry run successful even for a value the real write rejects. Use --strict-json (or config patch --file --dry-run) when you need schema validation.
openclaw config set channels.discord.token \
--ref-provider default \
--ref-source env \
--ref-id DISCORD_BOT_TOKEN \
--dry-run \
--json
openclaw config set channels.discord.token \
--ref-provider vault \
--ref-source exec \
--ref-id discord/token \
--dry-run \
--allow-exec
- Value mode (a plain `` without `--strict-json`): skips the full schema pass and ordinary SecretRef resolvability scan. Policy, provider, and model-reference checks can still run. When no checks apply, the CLI prints `Dry run note: value mode does not run schema/resolvability checks` and can succeed even when the real write would fail schema validation.
- Builder mode: runs SecretRef resolvability checks for changed refs/providers.
- JSON mode (`--strict-json`, `--json`, or batch mode): runs schema validation plus SecretRef resolvability checks.
- Policy validation runs against the full post-change config, so parent-object writes (for example setting `hooks` as an object) cannot bypass unsupported-surface validation.
- Exec command-path trust checks run without executing providers. Exec SecretRef resolvability checks are skipped by default to avoid command side effects; pass `--allow-exec` to opt in (this may execute provider commands). `--allow-exec` is dry-run only and errors without `--dry-run`.
- `ok`: whether dry-run passed
- `operations`: number of assignments evaluated
- `checks`: whether schema/resolvability checks ran
- `checks.resolvabilityComplete`: whether resolvability checks ran to completion (false when exec refs are skipped)
- `refsChecked`: number of refs actually resolved during dry-run
- `skippedExecRefs`: number of exec refs skipped because `--allow-exec` was not set
- `errors`: structured failures when `ok=false`; each carries a `kind` of `missing-path`, `schema`, `resolvability`, `model`, or `conflict` (`conflict` means the config file changed while the command was writing, so nothing was changed — re-run to pick up the new file)
JSON output shape
{
ok: boolean,
operations: number,
configPath: string,
inputModes: ["value" | "json" | "builder" | "unset", ...],
checks: {
schema: boolean,
resolvability: boolean,
resolvabilityComplete: boolean,
},
refsChecked: number,
skippedExecRefs: number,
errors?: [
{
kind: "missing-path" | "schema" | "resolvability" | "model" | "conflict",
message: string,
ref?: string, // present for resolvability errors
},
],
}
```json
{
"ok": true,
"operations": 1,
"configPath": "/home/user/.openclaw/openclaw.json",
"inputModes": ["builder"],
"checks": {
"schema": false,
"resolvability": true,
"resolvabilityComplete": true
},
"refsChecked": 1,
"skippedExecRefs": 0
}
```
```json
{
"ok": false,
"operations": 1,
"configPath": "/home/user/.openclaw/openclaw.json",
"inputModes": ["builder"],
"checks": {
"schema": false,
"resolvability": true,
"resolvabilityComplete": true
},
"refsChecked": 1,
"skippedExecRefs": 0,
"errors": [
{
"kind": "resolvability",
"message": "Error: Environment variable \"MISSING_TEST_SECRET\" is not set.",
"ref": "env:default:MISSING_TEST_SECRET"
}
]
}
```
- `config schema validation failed`: your post-change config shape is invalid; fix the path/value or provider/ref object shape.
- `Config policy validation failed: unsupported SecretRef usage`: move that credential back to plaintext/string input; keep SecretRefs on supported surfaces only.
- `SecretRef assignment(s) could not be resolved`: the referenced provider/ref cannot currently resolve (missing env/store name, invalid file pointer, exec provider failure, or provider/source mismatch).
- `model reference validation failed`: a changed text-model primary or fallback is unknown; run `openclaw models list` and choose an available model.
- `Dry run note: skipped exec SecretRef resolvability check(s)`: rerun with `--allow-exec` if you need exec resolvability validation.
- For batch mode, fix failing entries and rerun `--dry-run` before writing.
Applying changes
After every successful config set / config patch / config unset, the CLI prints one of three hints so you know whether the gateway needs a restart:
| Hint | Meaning |
|---|---|
Restart the gateway to apply. |
The changed path needs a full restart, or passive reload is disabled. |
Change will apply without restarting the gateway. |
Hot reload picks it up automatically. |
No gateway restart needed. |
Nothing runtime-relevant changed. |
Plugin entry changes use the same reload planner as other settings. In the default
hybrid mode, ordinary plugins.entries.<id> edits replace the affected plugin
instance automatically. A plugin's narrower restart policy or
gateway.reload.mode: "off" can still require a Gateway restart. The CLI hint
describes expected application; it is not a receipt from a running Gateway.
See Config hot reload.
Successful config set or config unset operations that produce no effective config diff print No change and leave the JSON5 file byte-for-byte untouched. A config unset target that is absent from the authored config exits with status 1 and also leaves the file untouched. Setting an absent key to a value equal to its runtime default is still an authored change and persists the explicit value.
Write safety
openclaw config set and other OpenClaw-owned config writers validate the full post-change config before committing it to disk. If the new payload fails schema validation or looks like a destructive clobber, the active config is left alone and the rejected payload is saved beside it as openclaw.json.rejected.*.
If staging a config save fails, the existing root or include backup ring is left untouched. OpenClaw prepares backup contents without blocking unrelated Gateway requests. If a copy fallback removes the file before a conflict, OpenClaw restores the original when it still owns the missing destination. Otherwise, the error reports partial publication and the backup location to inspect before another save.
If the file is saved but later processing fails, the error names the written file and reports whether the write was rolled back. This can name an included file when that file owns the edited setting. If rollback did not happen or could not be confirmed, inspect the named file and the active config before retrying.
OpenClaw-owned writes that change config reserialize JSON5 as standard JSON. When the source contains comments, the writer warns immediately before removing them; use a direct editor when preserving comments matters.
The active config path must be a regular file. Symlinked `openclaw.json` layouts are unsupported for writes; use `OPENCLAW_CONFIG_PATH` to point directly at the real file instead.Prefer CLI writes for small edits:
openclaw config set gateway.reload.mode '"hybrid"' --strict-json --dry-run
openclaw config set gateway.reload.mode '"hybrid"' --strict-json
openclaw config validate
If a write is rejected, inspect the saved payload and fix the full config shape:
CONFIG="$(openclaw config file)"
ls -lt "$CONFIG".rejected.* 2>/dev/null | head
openclaw config validate
Direct editor writes are still allowed, but the running Gateway treats them as untrusted until they validate. At startup, eligible single-file configs can receive deterministic legacy-key migrations if the complete result validates, with the previous config kept in the .bak ring. Other invalid direct edits fail startup; hot reload skips invalid edits without rewriting openclaw.json. Run openclaw doctor --fix to repair prefixed/clobbered config or restore the last-known-good copy. See Gateway troubleshooting.
Whole-file recovery is reserved for doctor repair. Plugin schema changes or minHostVersion skew stay loud instead of rolling back unrelated user settings such as models, providers, auth profiles, channels, gateway exposure, tools, memory, browser, or cron config.
Repair loop
After openclaw config validate passes, use the local TUI to have an embedded agent compare the active config against the docs while you validate each change from the same terminal:
openclaw chat
Inside the TUI, a leading ! runs a literal local shell command (after a one-time per-session confirmation prompt):
!openclaw config file
!openclaw docs gateway auth token secretref
!openclaw config validate
!openclaw doctor
Ask the agent to compare your current config with the relevant docs page and suggest the smallest fix.
Apply targeted edits with `openclaw config set` or `openclaw configure`.
Rerun `openclaw config validate` after each change.
If validation passes but the runtime is still unhealthy, run `openclaw doctor` or `openclaw doctor --fix` for migration and repair help.
Related
- CLI reference
- Configuration
openclaw configure— guided editor for the same settings