openclaw - ✅(Solved) Fix update: doctor injects Zod defaults that refreshGatewayServiceEnv validation rejects, blocking service restart [1 pull requests, 1 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#56772Fetched 2026-04-08 01:48:00
View on GitHub
Comments
1
Participants
2
Timeline
15
Reactions
1
Author
Participants
Timeline (top)
referenced ×10cross-referenced ×2closed ×1commented ×1

openclaw update fails to restart the gateway service after a successful binary update because openclaw doctor --non-interactive (run as step 2 of every update) materializes Zod .default() values into the config file, and the subsequent refreshGatewayServiceEnvgateway install --force validation rejects those same keys as "Unrecognized." The service restart code never executes, leaving the gateway dead after every update.

Error Message

Config overwrite: /Users/.../.openclaw/openclaw.json (sha256 ... -> ..., backup=...) Error: Config validation failed: channels.bluebubbles.accounts.default: Unrecognized key: "enrichGroupParticipantsFromContacts"

Root Cause

Two code paths disagree on schema scope:

Doctor (runs as subprocess with full plugin initialization):

  • readConfigFileSnapshot()validateConfigObjectWithPlugins(raw, { applyDefaults: true })
  • BlueBubbles plugin schema includes: enrichGroupParticipantsFromContacts: z.boolean().optional().default(true) (config-schema-B0e8FgTR.js:49)
  • Zod .parse() injects the default value when the key is absent
  • Doctor writes the inflated config back to disk

refreshGatewayServiceEnv (runs in-process without full plugin initialization):

  • Validates the doctor-modified config against the core channel schema
  • Core schema doesn't include BlueBubbles plugin-specific keys
  • Rejects enrichGroupParticipantsFromContacts as "Unrecognized key"

Same binary, two different schema scopes. Doctor adds keys the validator doesn't know about.

Fix Action

Workaround

Manually removing the injected keys from openclaw.json after each update allows the next startup to succeed, but doctor re-adds them on every subsequent update, making this unsustainable.

PR fix notes

PR #56834: fix(config): prevent AJV schema defaults from leaking into persisted config

Description (problem / solution / changelog)

Summary

  • Problem: When running openclaw update, the doctor command reads, modifies, and writes the configuration. After this, the gateway service fails to restart and enters a crash loop because the core Zod schema rejects unknown keys (e.g., enrichGroupParticipantsFromContacts: true from the BlueBubbles plugin) that were unexpectedly injected into openclaw.json. Furthermore, in --json mode, the refreshGatewayServiceEnv failure is silently swallowed in src/cli/update-cli/update-command.ts (around line 637), hiding the root cause from auto-update callers.

  • Root Cause: During the config write path in src/config/io.ts (writeConfigFile), the code was using validated.config as the payload to write to disk. Because the internal AJV validation for plugins/channels injects schema defaults (e.g., enrichGroupParticipantsFromContacts: true for BlueBubbles), these runtime defaults were leaking into the persisted openclaw.json.

  • Fix: i. Changed cfgToWrite to use persistCandidate (the raw merge-patched value before validation) instead of validated.config in src/config/io.ts. This is a defensive design choice ensuring that validation side-effects (like AJV default injection) can never leak into the persistence payload. ii. Applied normalizeLegacyWebSearchConfig to persistCandidate before writing, ensuring that legacy web-search migrations are still correctly persisted even though we bypass the full validation pipeline. iii. Added explicit error logging for refreshGatewayServiceEnv in src/cli/update-cli/update-command.ts to ensure failures are visible even in --json mode.

  • What changed:

    • src/config/io.ts: Modified writeConfigFile to persist normalizeLegacyWebSearchConfig(persistCandidate) instead of validated.config.
    • src/cli/update-cli/update-command.ts: Added stderr logging for service refresh failures in JSON mode.
    • src/config/io.write-config.test.ts: Added regression test using a mock plugin registry to ensure AJV defaults do not leak into persisted config.
    • src/config/validation.channel-metadata.test.ts: Updated tests to reflect that AJV default injection during validation is intentional and safe.
  • What did NOT change (scope boundary): The core Zod schema validation logic and its .strict() enforcement remain unchanged. src/config/validation.ts was deliberately NOT modified to disable applyDefaults, as doing so would break write-back flows for schemas that have fields marked as both default and required.

Reproduction

  1. Install OpenClaw and configure a plugin that utilizes AJV schema defaults (e.g., BlueBubbles).
  2. Run openclaw update (which triggers the doctor read-modify-write cycle).
  3. Open ~/.openclaw/openclaw.json and observe that it now contains injected default keys (e.g., enrichGroupParticipantsFromContacts: true).
  4. Attempt to restart the gateway service; observe that it crashes because the strict Zod schema rejects the newly injected unknown keys.

Risk / Mitigation

  • Risk: Bypassing validated.config for persistence might skip intentional normalizations.
  • Mitigation: We explicitly added normalizeLegacyWebSearchConfig to the write path to preserve the only necessary normalization. Furthermore, a comprehensive regression test was added in src/config/io.write-config.test.ts that simulates the exact read-modify-write cycle of the doctor command, verifying that no defaults leak and the resulting config remains valid.

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • Gateway / orchestration
  • App: web-ui

Linked Issue/PR

Fixes #56772

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/cli/update-cli/update-command.ts (modified, +8/-6)
  • src/config/io.ts (modified, +8/-3)
  • src/config/io.write-config.test.ts (modified, +106/-0)
  • src/config/validation.channel-metadata.test.ts (modified, +59/-26)

Code Example

Config overwrite: /Users/.../.openclaw/openclaw.json (sha256 ... -> ..., backup=...)
     Error: Config validation failed: channels.bluebubbles.accounts.default: Unrecognized key: "enrichGroupParticipantsFromContacts"
RAW_BUFFERClick to expand / collapse

Summary

openclaw update fails to restart the gateway service after a successful binary update because openclaw doctor --non-interactive (run as step 2 of every update) materializes Zod .default() values into the config file, and the subsequent refreshGatewayServiceEnvgateway install --force validation rejects those same keys as "Unrecognized." The service restart code never executes, leaving the gateway dead after every update.

Environment

  • OpenClaw: 2026.3.28 (f9b1079)
  • Node: v22.22.0
  • macOS: 26.3 (25D125), Apple Silicon
  • Install: npm global (/opt/homebrew/lib/node_modules/openclaw)
  • Service: launchd LaunchAgent (ai.openclaw.gateway)
  • Channel config: BlueBubbles with multi-account (accounts.default + accounts.chip)

Reproduction

  1. Have a BlueBubbles channel config with accounts (multi-account setup)
  2. Ensure enrichGroupParticipantsFromContacts is NOT explicitly in the config
  3. Run: OPENCLAW_AUTO_UPDATE=1 openclaw update --yes --channel stable --json
  4. Observe:
    • npm install succeeds (exit 0)
    • openclaw doctor --non-interactive succeeds (exit 0) — but overwrites openclaw.json, injecting enrichGroupParticipantsFromContacts: true at 3 locations
    • After JSON output completes:
      Config overwrite: /Users/.../.openclaw/openclaw.json (sha256 ... -> ..., backup=...)
      Error: Config validation failed: channels.bluebubbles.accounts.default: Unrecognized key: "enrichGroupParticipantsFromContacts"

The error comes from refreshGatewayServiceEnv (update-cli, ~line 949), which is caught and silently suppressed in --json mode. Steps 4–5 (bootout + bootstrap + restart script) never execute. The binary is replaced but the service is never restarted.

Root cause

Two code paths disagree on schema scope:

Doctor (runs as subprocess with full plugin initialization):

  • readConfigFileSnapshot()validateConfigObjectWithPlugins(raw, { applyDefaults: true })
  • BlueBubbles plugin schema includes: enrichGroupParticipantsFromContacts: z.boolean().optional().default(true) (config-schema-B0e8FgTR.js:49)
  • Zod .parse() injects the default value when the key is absent
  • Doctor writes the inflated config back to disk

refreshGatewayServiceEnv (runs in-process without full plugin initialization):

  • Validates the doctor-modified config against the core channel schema
  • Core schema doesn't include BlueBubbles plugin-specific keys
  • Rejects enrichGroupParticipantsFromContacts as "Unrecognized key"

Same binary, two different schema scopes. Doctor adds keys the validator doesn't know about.

Impact

Every openclaw update (manual or auto):

  1. Replaces the binary on disk ✓
  2. Runs doctor which injects defaults into config ✓
  3. Fails validation → skips service restart ✗
  4. Old gateway process eventually crashes (stale module references — hash-based filenames no longer exist)
  5. launchd restarts with new binary → new binary hits same config validation error → crash loop
  6. launchd classifies service as "inefficient" → permanently removes it from job table
  7. Gateway stays dead until manual launchctl load or openclaw gateway install

This has caused repeated multi-hour outages. The auto-update state is never written to update-check.json (gateway dies before the await writeState() at line 1530 executes), so there's no record of the failures.

Suggested fixes

  1. Don't materialize Zod defaults into persisted config: readConfigFileSnapshot() should return raw config (or strip keys matching schema defaults before doctor writes back)
  2. refreshGatewayServiceEnv should validate with full plugin schemas: Use the same validateConfigObjectWithPlugins as doctor, not the core-only validator
  3. Don't silently suppress refreshGatewayServiceEnv failures in --json mode: At minimum, include the error in the JSON output so auto-update callers can detect the failure

Workaround

Manually removing the injected keys from openclaw.json after each update allows the next startup to succeed, but doctor re-adds them on every subsequent update, making this unsustainable.

Related issues

  • #22272 (doctor --fix identifies unrecognized keys but doesn't remove them)
  • #29780 (runtime writes invalid keys into config, causing crash loop)
  • #46955 (update reinstalls plugins and rewrites openclaw.json on every run)
  • #35957 (config migration introduces keys rejected by new version)

extent analysis

Fix Plan

To address the issue, we will implement the following steps:

  • Modify the readConfigFileSnapshot() function to return the raw config without materializing Zod defaults.
  • Update the refreshGatewayServiceEnv function to validate the config with full plugin schemas.
  • Remove silent suppression of refreshGatewayServiceEnv failures in --json mode.

Code Changes

// Modify readConfigFileSnapshot() to return raw config
function readConfigFileSnapshot() {
  const rawConfig = fs.readFileSync('openclaw.json', 'utf8');
  return JSON.parse(rawConfig);
}

// Update refreshGatewayServiceEnv to validate with full plugin schemas
function refreshGatewayServiceEnv(config) {
  const validator = validateConfigObjectWithPlugins(config, { applyDefaults: true });
  if (!validator.success) {
    throw new Error(`Config validation failed: ${validator.error}`);
  }
  // ...
}

// Remove silent suppression of refreshGatewayServiceEnv failures in --json mode
if (process.argv.includes('--json')) {
  try {
    // ...
  } catch (error) {
    console.error(`Error: ${error.message}`);
    process.exit(1);
  }
}

Verification

To verify the fix, run the following steps:

  1. Update the openclaw binary with the modified code.
  2. Run openclaw update --yes --channel stable --json to test the update process.
  3. Verify that the gateway service restarts successfully after the update.
  4. Check the openclaw.json file to ensure that Zod defaults are not materialized.

Extra Tips

  • Regularly review and update the openclaw configuration to prevent similar issues.
  • Consider implementing automated testing for the openclaw update process to catch similar errors.
  • Refer to the related issues (#22272, #29780, #46955, #35957) for additional context and potential fixes.

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