openclaw - ✅(Solved) Fix [Bug]: Control UI Raw mode permanently disabled since 2026.3.31 — materializeRuntimeConfig injects undefined keys that break round-trip check [1 pull requests, 4 comments, 2 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#59330Fetched 2026-04-08 02:25:55
View on GitHub
Comments
4
Participants
2
Timeline
7
Reactions
4
Author
Timeline (top)
cross-referenced ×3commented ×2referenced ×1subscribed ×1

Root Cause

normalizeExecSafeBinProfilesInConfig() (introduced between 3.28 and 3.31) sets safeBinProfiles and safeBinTrustedDirs to void 0 (undefined) when they are empty:

// src/config/normalize-exec-safe-bin.ts (in io module, line ~16799)
typedExec.safeBinProfiles = Object.keys(normalizedProfiles).length > 0 ? normalizedProfiles : void 0;
typedExec.safeBinTrustedDirs = normalizedTrustedDirs.length > 0 ? normalizedTrustedDirs : void 0;

This creates keys with undefined values on snapshot.config.tools.exec. When shouldFallbackToStructuredRawRedaction() compares the restored (JSON-parsed) object against snapshot.config using isDeepStrictEqual:

  • JSON-parsed object: tools.exec has no safeBinProfiles key (JSON cannot represent undefined)
  • snapshot.config: tools.exec has safeBinProfiles: undefined (key exists, value is undefined)
  • isDeepStrictEqual({safeBinProfiles: undefined}, {})false

This means the round-trip check always fails, raw is set to null, and Raw mode is permanently disabled.

Fix Action

Fixed

PR fix notes

PR #59336: fix: Config Raw mode permanently disabled due to round-trip check regression

Description (problem / solution / changelog)

Summary

Fixes #59330 — Control UI Config editor permanently shows "Raw mode disabled (snapshot cannot safely round-trip raw text)" since 2026.3.31.

Root Cause

Two issues cause shouldFallbackToStructuredRawRedaction() to always return true:

1. normalizeExecSafeBinProfilesInConfig assigns void 0 instead of deleting keys

When safeBinProfiles/safeBinTrustedDirs are empty, the normalize function assigns undefined:

typedExec.safeBinProfiles = ... ? normalizedProfiles : undefined;

This creates keys with undefined values. JSON-parsed objects can never have undefined-valued keys, so isDeepStrictEqual() always fails:

isDeepStrictEqual({ safeBinProfiles: undefined }, {}) // → false

2. shouldFallbackToStructuredRawRedaction compares against materialized config

The round-trip check compares the restored (raw-parsed) config against snapshot.config (the materialized runtime config), which contains transformations not present in the raw source:

  • ~/ paths expanded to absolute paths
  • Model api field inherited from provider level
  • Auto-generated alias fields on model entries
  • Default empty config: {} added to plugin entries

The comparison should use snapshot.parsed (the source config before materialization).

Changes

  1. src/config/normalize-exec-safe-bin.ts: Use delete instead of assigning undefined when profiles/dirs are empty.

  2. src/config/redact-snapshot.raw.ts:

    • Accept optional sourceConfig parameter for comparison target
    • Normalize both sides with JSON.parse(JSON.stringify()) to strip any remaining undefined-valued keys
  3. src/config/redact-snapshot.ts: Pass snapshot.parsed as sourceConfig to the round-trip check.

Testing

  • Verified locally: redactConfigSnapshot() now returns raw as a string (not null)
  • Raw mode is re-enabled in Control UI after these changes
  • Confirmed the fix handles all materialization diffs (path expansion, field inheritance, alias generation, default objects)

🤖 AI-assisted (Claude via OpenClaw). Fully tested locally against real config. I understand what the code does.

Changed files

  • src/config/normalize-exec-safe-bin.ts (modified, +10/-4)
  • src/config/redact-snapshot.raw.ts (modified, +26/-1)
  • src/config/redact-snapshot.ts (modified, +1/-0)

Code Example

// src/config/normalize-exec-safe-bin.ts (in io module, line ~16799)
typedExec.safeBinProfiles = Object.keys(normalizedProfiles).length > 0 ? normalizedProfiles : void 0;
typedExec.safeBinTrustedDirs = normalizedTrustedDirs.length > 0 ? normalizedTrustedDirs : void 0;

---

const { isDeepStrictEqual } = require("util");

// This is what materializeRuntimeConfig produces:
const a = { security: "full", ask: "off", safeBinProfiles: undefined };
// This is what JSON.parse(JSON.stringify(raw)) produces:
const b = { security: "full", ask: "off" };

console.log(isDeepStrictEqual(a, b)); // false — round-trip always fails

---

const snapshot = await readConfigFileSnapshot();
console.log("safeBinProfiles" in snapshot.config.tools.exec);   // true
console.log(snapshot.config.tools.exec.safeBinProfiles);         // undefined

const { a: redactConfigSnapshot } = await import("runtime-schema");
const result = redactConfigSnapshot(snapshot, uiHints);
console.log(result.raw === null);  // true — Raw mode disabled

---

delete snapshot.config.tools.exec.safeBinProfiles;
delete snapshot.config.tools.exec.safeBinTrustedDirs;
const result = redactConfigSnapshot(snapshot, uiHints);
console.log(result.raw === null);  // false — Raw mode works!

---

// Before (broken):
typedExec.safeBinProfiles = Object.keys(normalizedProfiles).length > 0 ? normalizedProfiles : void 0;
typedExec.safeBinTrustedDirs = normalizedTrustedDirs.length > 0 ? normalizedTrustedDirs : void 0;

// After (fixed):
if (Object.keys(normalizedProfiles).length > 0) typedExec.safeBinProfiles = normalizedProfiles;
else delete typedExec.safeBinProfiles;

if (normalizedTrustedDirs.length > 0) typedExec.safeBinTrustedDirs = normalizedTrustedDirs;
else delete typedExec.safeBinTrustedDirs;
RAW_BUFFERClick to expand / collapse

Bug Description

Since 2026.3.31, the Control UI Config editor permanently shows "Raw mode disabled (snapshot cannot safely round-trip raw text)" and forces Form-only mode. This is a regression — it worked correctly in 2026.3.28.

Root Cause

normalizeExecSafeBinProfilesInConfig() (introduced between 3.28 and 3.31) sets safeBinProfiles and safeBinTrustedDirs to void 0 (undefined) when they are empty:

// src/config/normalize-exec-safe-bin.ts (in io module, line ~16799)
typedExec.safeBinProfiles = Object.keys(normalizedProfiles).length > 0 ? normalizedProfiles : void 0;
typedExec.safeBinTrustedDirs = normalizedTrustedDirs.length > 0 ? normalizedTrustedDirs : void 0;

This creates keys with undefined values on snapshot.config.tools.exec. When shouldFallbackToStructuredRawRedaction() compares the restored (JSON-parsed) object against snapshot.config using isDeepStrictEqual:

  • JSON-parsed object: tools.exec has no safeBinProfiles key (JSON cannot represent undefined)
  • snapshot.config: tools.exec has safeBinProfiles: undefined (key exists, value is undefined)
  • isDeepStrictEqual({safeBinProfiles: undefined}, {})false

This means the round-trip check always fails, raw is set to null, and Raw mode is permanently disabled.

Reproduction

const { isDeepStrictEqual } = require("util");

// This is what materializeRuntimeConfig produces:
const a = { security: "full", ask: "off", safeBinProfiles: undefined };
// This is what JSON.parse(JSON.stringify(raw)) produces:
const b = { security: "full", ask: "off" };

console.log(isDeepStrictEqual(a, b)); // false — round-trip always fails

Full reproduction script:

const snapshot = await readConfigFileSnapshot();
console.log("safeBinProfiles" in snapshot.config.tools.exec);   // true
console.log(snapshot.config.tools.exec.safeBinProfiles);         // undefined

const { a: redactConfigSnapshot } = await import("runtime-schema");
const result = redactConfigSnapshot(snapshot, uiHints);
console.log(result.raw === null);  // true — Raw mode disabled

Deleting the undefined keys before comparison confirms this is the sole cause:

delete snapshot.config.tools.exec.safeBinProfiles;
delete snapshot.config.tools.exec.safeBinTrustedDirs;
const result = redactConfigSnapshot(snapshot, uiHints);
console.log(result.raw === null);  // false — Raw mode works!

Suggested Fix

In normalizeExecSafeBinProfilesInConfig, use delete instead of assigning void 0:

// Before (broken):
typedExec.safeBinProfiles = Object.keys(normalizedProfiles).length > 0 ? normalizedProfiles : void 0;
typedExec.safeBinTrustedDirs = normalizedTrustedDirs.length > 0 ? normalizedTrustedDirs : void 0;

// After (fixed):
if (Object.keys(normalizedProfiles).length > 0) typedExec.safeBinProfiles = normalizedProfiles;
else delete typedExec.safeBinProfiles;

if (normalizedTrustedDirs.length > 0) typedExec.safeBinTrustedDirs = normalizedTrustedDirs;
else delete typedExec.safeBinTrustedDirs;

Alternatively, shouldFallbackToStructuredRawRedaction could strip undefined-valued keys before calling isDeepStrictEqual.

Additional Context

There are also other (pre-existing) materialization diffs that the round-trip check catches, though they are individually insufficient to trigger the bug once safeBin keys are fixed:

  • browser.profiles.*.userDataDir: ~ expanded to full path
  • models.providers.kimi.models[0].api: inherited from provider-level api
  • agents.defaults.models.*.alias: auto-generated alias fields
  • plugins.entries.browser.config: default empty object added

These could also cause Raw mode to be disabled depending on the config. The round-trip check may be too strict — comparing only sensitive field restoration rather than full isDeepStrictEqual would be more robust.

Environment

  • OpenClaw: 2026.4.1 (da64a97)
  • Last working version: 2026.3.28
  • OS: macOS (arm64, Darwin 25.4.0)
  • Node: v25.6.0

extent analysis

TL;DR

The most likely fix is to modify the normalizeExecSafeBinProfilesInConfig function to use delete instead of assigning void 0 to remove empty properties.

Guidance

  • Identify the normalizeExecSafeBinProfilesInConfig function in the codebase and update it to use delete for removing empty properties, as suggested in the issue.
  • Verify that the round-trip check in shouldFallbackToStructuredRawRedaction is working correctly after the fix by checking if Raw mode is enabled.
  • Consider reviewing the round-trip check to make it more robust by comparing only sensitive field restoration rather than full isDeepStrictEqual.
  • Test the fix with different configurations to ensure that Raw mode is not disabled unnecessarily.

Example

// Fixed code
if (Object.keys(normalizedProfiles).length > 0) typedExec.safeBinProfiles = normalizedProfiles;
else delete typedExec.safeBinProfiles;

if (normalizedTrustedDirs.length > 0) typedExec.safeBinTrustedDirs = normalizedTrustedDirs;
else delete typedExec.safeBinTrustedDirs;

Notes

The fix assumes that the issue is solely caused by the normalizeExecSafeBinProfilesInConfig function assigning void 0 to empty properties. However, there may be other pre-existing materialization diffs that could still cause Raw mode to be disabled.

Recommendation

Apply the suggested fix to the normalizeExecSafeBinProfilesInConfig function to remove empty properties using delete instead of assigning void 0, as this is the most direct solution to the identified issue.

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