openclaw - 💡(How to fix) Fix Silent config data loss: Zod `.strict()` strips unknown top-level fields on round-trip [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#70578Fetched 2026-04-24 05:56:08
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0
Author
Participants

Error Message

Only one schema exception exists in the codebase: HookConfigSchema at line 389 uses .passthrough() to preserve unknown plugin-owned fields. That pattern is not applied to the root config. Commands that ship bundled with OpenClaw — doctor, onboarding, audit, wiki (registered as command aliases on plugins like memory-wiki) — are blocked by the user's plugins.allow if it's non-empty and narrow. Users then have to manually add each bundled command to their own allow list whenever they hit this error.

Root Cause

The audit log system already knows this is happening — it tags each write with size-drop-vs-last-good:<prev>-><next>. The tags have been accumulating since at least March. The system can detect data loss but not prevent or restore. For any user with non-trivial config (multiple channels, custom models, legacy entries from older schema versions), a single plugins install or pre-4.15 reboot silently deletes the content with no undo.

Fix Action

Workaround

For users hit by this now:

  1. Never run openclaw plugins install again — each call strips ~21 KB of config.
  2. Set plugins.allow: [] (empty) to bypass the CLI-surface check until Bug 3 is fixed — downside is a log warning about non-bundled plugin auto-load.
  3. Apply the root-schema .passthrough() patch manually in node_modules/openclaw/dist/zod-schema-*.js to stop future strips. Re-apply after every npm update -g openclaw.

Code Example

const OpenClawSchema = z.object({
    $schema: z.string().optional(),
    meta: z.object({...}).strict().optional(),
    env: z.object({...}).optional(),
    // ... ~15 top-level sections, 126 total nested .strict() calls ...
}).strict().superRefine((cfg, ctx) => { ... })

---

function projectSourceOntoRuntimeShape(source, runtime) {
    if (!isRecord(source) || !isRecord(runtime)) return cloneUnknown(source);
    const next = {};
    for (const [key, sourceValue] of Object.entries(source)) {
        if (!(key in runtime)) continue;  // drops unknown keys
        next[key] = projectSourceOntoRuntimeShape(sourceValue, runtime[key]);
    }
    return next;
}

---

if (allow.length > 0 && !allow.includes(normalizedPluginId)) {
    return `The \`openclaw ${normalizedPluginId}\` command is unavailable because \`plugins.allow\` excludes "${normalizedPluginId}". Add "${normalizedPluginId}" to \`plugins.allow\` if you want that bundled plugin CLI surface.`;
}
RAW_BUFFERClick to expand / collapse

Silent config data loss: Zod .strict() + projectSourceOntoRuntimeShape + CLI-surface allowlist design

TL;DR

On OpenClaw 2026.4.15 (Windows, global npm install), my openclaw.json has been silently stripped from ~34 KB to ~13 KB on five occasions since 2026-03-22, triggered by openclaw plugins install <path> and (pre-4.15) openclaw gateway startup. Audit log (logs/config-audit.jsonl) flagged each drop with "suspicious": ["size-drop-vs-last-good:34738->13xxx"] — the system noticed the loss but didn't prevent or revert it.

Investigation traces the root cause to three separate mechanisms that together make openclaw.json a lossy store for any field the current schema doesn't explicitly recognize.

Evidence — audit log chronology

Extracted from G:\.openclaw\logs\config-audit.jsonl:

Date (UTC)TriggerPreviousNext
2026-03-22 20:17plugins install @vectorize-io/hindsight-openclaw24828 B9683 B
2026-04-06 20:02plugins install C:\...\waifuclaw-plugin32332 B12150 B
2026-04-07 05:30gateway --port 18789 (boot)35239 B13096 B
2026-04-10 04:59gateway (boot)35295 B13476 B
2026-04-12 09:35gateway --port 18789 (boot)34738 B13248 B

After 2026-04-12, gateway boots stop writing config (presumably the hash-guard in #67557 / v2026.4.15 .beta.1 prevents the spurious write loop). plugins install still strips, untested post-4.12 because I've stopped running it.

Bug 1 — Root OpenClawSchema uses .strict(), silently drops unknown fields

File: dist/zod-schema-BO9ySEsE.js:1298 (filename hash may vary per build) Also called from: dist/io-5pxHCi7V.js:17589 via OpenClawSchema.safeParse(raw) inside validateConfigObjectRaw

const OpenClawSchema = z.object({
    $schema: z.string().optional(),
    meta: z.object({...}).strict().optional(),
    env: z.object({...}).optional(),
    // ... ~15 top-level sections, 126 total nested .strict() calls ...
}).strict().superRefine((cfg, ctx) => { ... })

.strict() in Zod rejects parsing when unknown keys are present. In practice on OpenClaw's validation path, this manifests as validated.data containing only schema-known fields — anything outside the schema (legacy plugin entries, migrated-away settings, deprecated channel config) is lost on the round-trip through safeParse.

Only one schema exception exists in the codebase: HookConfigSchema at line 389 uses .passthrough() to preserve unknown plugin-owned fields. That pattern is not applied to the root config.

Suggested fix: change root .strict().passthrough() (or .catchall(z.unknown())). Keep nested .strict() validators intact. This preserves unknown top-level keys across round-trips without relaxing validation of known sections. Unknown keys become inert data; OpenClaw code only reads schema-validated paths.

Bug 2 — projectSourceOntoRuntimeShape() drops keys missing from runtime shape

File: dist/io-5pxHCi7V.js:3285 Called from: writeConfigFile() via resolvePersistCandidateForWrite()

function projectSourceOntoRuntimeShape(source, runtime) {
    if (!isRecord(source) || !isRecord(runtime)) return cloneUnknown(source);
    const next = {};
    for (const [key, sourceValue] of Object.entries(source)) {
        if (!(key in runtime)) continue;  // drops unknown keys
        next[key] = projectSourceOntoRuntimeShape(sourceValue, runtime[key]);
    }
    return next;
}

Even with Bug 1 fixed, this function would still strip keys that exist in the source file but not in the runtime model. The v2026.4.15 #67557 fix added a hash guard so plugin-auto-enable doesn't write on every boot — but when it DOES write (new candidates discovered), it still goes through this function.

Suggested fix: either preserve the source side of any top-level key the runtime model doesn't know about, or scope the projection to only the sections the runtime explicitly owns.

Bug 3 — Bundled CLI command surfaces subject to user plugins.allow

File: dist/run-main-BBeVm29G.js:374 inside resolveMissingPluginCommandMessage()

if (allow.length > 0 && !allow.includes(normalizedPluginId)) {
    return `The \`openclaw ${normalizedPluginId}\` command is unavailable because \`plugins.allow\` excludes "${normalizedPluginId}". Add "${normalizedPluginId}" to \`plugins.allow\` if you want that bundled plugin CLI surface.`;
}

Commands that ship bundled with OpenClaw — doctor, onboarding, audit, wiki (registered as command aliases on plugins like memory-wiki) — are blocked by the user's plugins.allow if it's non-empty and narrow. Users then have to manually add each bundled command to their own allow list whenever they hit this error.

This interacts badly with Bug 1: after config is stripped, plugins.allow shrinks to a narrow list, and the next attempt to run doctor / audit / onboarding fails, sending users on a discover-and-add-each-one loop.

Suggested fix: exempt bundled OpenClaw-owned command surfaces from the plugins.allow check, or seed the allow list at boot with the bundled CLI plugin IDs so user-configured narrow lists automatically include them.

Workaround

For users hit by this now:

  1. Never run openclaw plugins install again — each call strips ~21 KB of config.
  2. Set plugins.allow: [] (empty) to bypass the CLI-surface check until Bug 3 is fixed — downside is a log warning about non-bundled plugin auto-load.
  3. Apply the root-schema .passthrough() patch manually in node_modules/openclaw/dist/zod-schema-*.js to stop future strips. Re-apply after every npm update -g openclaw.

Environment

  • OpenClaw 2026.4.15 (build 041266a)
  • Node 22+
  • Windows 11 Home (npm global install)
  • Config at G:\.openclaw\openclaw.json
  • Extensions: [email protected], waifuclaw (workspace plugin)

Why this matters

The audit log system already knows this is happening — it tags each write with size-drop-vs-last-good:<prev>-><next>. The tags have been accumulating since at least March. The system can detect data loss but not prevent or restore. For any user with non-trivial config (multiple channels, custom models, legacy entries from older schema versions), a single plugins install or pre-4.15 reboot silently deletes the content with no undo.

extent analysis

TL;DR

To prevent silent config data loss in OpenClaw, apply the suggested fixes to the root schema, projectSourceOntoRuntimeShape function, and CLI command surface allowlist design.

Guidance

  1. Change the root schema to use .passthrough(): Update the OpenClawSchema definition to use .passthrough() instead of .strict() to preserve unknown top-level keys.
  2. Modify projectSourceOntoRuntimeShape to preserve unknown keys: Update the function to preserve keys that exist in the source file but not in the runtime model.
  3. Exempt bundled OpenClaw-owned command surfaces from the plugins.allow check: Update the resolveMissingPluginCommandMessage function to exempt bundled command surfaces from the plugins.allow check.
  4. Verify the fixes: After applying the fixes, run openclaw plugins install and check the audit log to ensure that the config data is no longer being stripped.
  5. Test the workaround: Apply the workaround by setting plugins.allow: [] and manually patching the root schema to stop future strips.

Example

// Update OpenClawSchema to use .passthrough()
const OpenClawSchema = z.object({
    $schema: z.string().optional(),
    meta: z.object({...}).optional(),
    env: z.object({...}).optional(),
    // ... ~15 top-level sections, 126 total nested .strict() calls ...
}).passthrough().superRefine((cfg, ctx) => { ... })

Notes

The suggested fixes may require updates to the OpenClaw codebase and may not be applicable to all versions. The workaround can be used to mitigate the issue until the fixes are applied.

Recommendation

Apply the workaround by setting plugins.allow: [] and manually patching the root schema to stop future strips. This will prevent further data loss until the fixes can be

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING