Bug: openclaw doctor --fix corrupts signal multi-account config by inventing a phantom accounts.default block
Environment
- OpenClaw
2026.4.5 (3e72c03)
- Ubuntu 24.04.4 LTS, Node v24.14.1 (via nvm)
- signal-cli 0.14.2 (native build) at
/usr/local/bin/signal-cli
- Two registered Signal accounts in multi-account configuration
Steps to reproduce
-
Configure channels.signal with two named accounts and channel-level shared defaults:
{
"channels": {
"signal": {
"enabled": true,
"cliPath": "signal-cli",
"dmPolicy": "pairing",
"groupPolicy": "disabled",
"defaultAccount": "pleres",
"accounts": {
"pleres": {
"account": "+15555550100",
"name": "Account A"
},
"mote": {
"account": "+15555550101",
"name": "Account B",
"httpPort": 8081
}
}
}
}
}
-
Verify the gateway is healthy with this config — both signal-cli daemons spawn cleanly on 127.0.0.1:8080 and 127.0.0.1:8081, both probes pass openclaw doctor (Signal: ok).
-
Run openclaw doctor --fix.
-
Doctor reports: "Moved channels.signal single-account top-level values into channels.signal.accounts.default." It writes a new openclaw.json with the following transformation:
{
"channels": {
"signal": {
"enabled": true,
"defaultAccount": "pleres",
"accounts": {
"pleres": { "account": "+15555550100", "name": "Account A" },
"mote": { "account": "+15555550101", "name": "Account B", "httpPort": 8081 },
"default": {
"cliPath": "signal-cli",
"dmPolicy": "pairing",
"groupPolicy": "disabled"
}
}
}
}
}
-
The accounts.default block has no account (phone number) field. It is a settings holder masquerading as an account.
-
Restart the gateway (or wait for hot reload).
Expected behavior
Either:
- (A) Doctor leaves the existing config alone — top-level
cliPath/dmPolicy/groupPolicy are valid channel-level defaults that named accounts can inherit, and the structure works in production.
- (B) If a transformation IS desired, doctor should move the shared defaults INTO each named account block (
accounts.pleres.dmPolicy, accounts.mote.dmPolicy, etc.), not invent a phantom accounts.default slot that has no real signal-cli account.
Actual behavior
After the transformation:
- Static config validation passes (
openclaw config validate reports OK).
- Runtime breaks: gateway enters a crash loop. signal-cli daemons fail to spawn (the
accounts.default block has no account field, the daemon-spawn pathway can't reconcile it).
- Reverting
openclaw.json to the pre---fix shape restores function.
Severity
High. openclaw doctor --fix is presented as the canonical "apply changes" path, and users will run it. The transformation causes complete signal channel outage with no actionable error. We had to back out by restoring an openclaw.json.bak from before the --fix run.
Workaround
Do not run openclaw doctor --fix on multi-account signal configs. Read-only openclaw doctor (without --fix) is fine for diagnostics; only the auto-apply path is dangerous. Apply doctor's recommendations manually after reading the proposed diff.
Suggested fix paths
- Detect that named accounts already exist; in that case, skip the "move single-account values" migration entirely (it's targeting a different config shape).
- If a settings-only inheritance block is desired, give it a different schema key (e.g.
channels.signal.accountDefaults) so it doesn't collide with real accounts.
- Preserve top-level
dmPolicy/groupPolicy/cliPath as legitimate channel-level defaults that all accounts inherit unless overridden — that's what the multi-account doc strongly implies should work.
Bug:
openclaw doctor --fixcorrupts signal multi-account config by inventing a phantomaccounts.defaultblockEnvironment
2026.4.5 (3e72c03)/usr/local/bin/signal-cliSteps to reproduce
Configure
channels.signalwith two named accounts and channel-level shared defaults:{ "channels": { "signal": { "enabled": true, "cliPath": "signal-cli", "dmPolicy": "pairing", "groupPolicy": "disabled", "defaultAccount": "pleres", "accounts": { "pleres": { "account": "+15555550100", "name": "Account A" }, "mote": { "account": "+15555550101", "name": "Account B", "httpPort": 8081 } } } } }Verify the gateway is healthy with this config — both signal-cli daemons spawn cleanly on 127.0.0.1:8080 and 127.0.0.1:8081, both probes pass
openclaw doctor(Signal: ok).Run
openclaw doctor --fix.Doctor reports: "Moved channels.signal single-account top-level values into channels.signal.accounts.default." It writes a new openclaw.json with the following transformation:
{ "channels": { "signal": { "enabled": true, "defaultAccount": "pleres", "accounts": { "pleres": { "account": "+15555550100", "name": "Account A" }, "mote": { "account": "+15555550101", "name": "Account B", "httpPort": 8081 }, "default": { "cliPath": "signal-cli", "dmPolicy": "pairing", "groupPolicy": "disabled" } } } } }The
accounts.defaultblock has noaccount(phone number) field. It is a settings holder masquerading as an account.Restart the gateway (or wait for hot reload).
Expected behavior
Either:
cliPath/dmPolicy/groupPolicyare valid channel-level defaults that named accounts can inherit, and the structure works in production.accounts.pleres.dmPolicy,accounts.mote.dmPolicy, etc.), not invent a phantomaccounts.defaultslot that has no real signal-cli account.Actual behavior
After the transformation:
openclaw config validatereports OK).accounts.defaultblock has noaccountfield, the daemon-spawn pathway can't reconcile it).openclaw.jsonto the pre---fixshape restores function.Severity
High.
openclaw doctor --fixis presented as the canonical "apply changes" path, and users will run it. The transformation causes complete signal channel outage with no actionable error. We had to back out by restoring anopenclaw.json.bakfrom before the--fixrun.Workaround
Do not run
openclaw doctor --fixon multi-account signal configs. Read-onlyopenclaw doctor(without--fix) is fine for diagnostics; only the auto-apply path is dangerous. Apply doctor's recommendations manually after reading the proposed diff.Suggested fix paths
channels.signal.accountDefaults) so it doesn't collide with real accounts.dmPolicy/groupPolicy/cliPathas legitimate channel-level defaults that all accounts inherit unless overridden — that's what the multi-account doc strongly implies should work.