Skip to content

Commit b6c8d1e

Browse files
committed
fix(cli): add strict-json config set flag
1 parent e2ff28a commit b6c8d1e

File tree

4 files changed

+59
-7
lines changed

4 files changed

+59
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
1818
- Docker: pin base images to SHA256 digests in Docker builds to prevent mutable tag drift. (#7734) Thanks @coygeek.
1919
- Provider/HTTP: treat HTTP 503 as failover-eligible for LLM provider errors. (#21086) Thanks @Protocol-zero-0.
2020
- Slack: pass `recipient_team_id` / `recipient_user_id` through Slack native streaming calls so `chat.startStream`/`appendStream`/`stopStream` work reliably across DMs and Slack Connect setups, and disable block streaming when native streaming is active. (#20988) Thanks @Dithilli. Earlier recipient-ID groundwork was contributed in #20377 by @AsserAl1012.
21+
- CLI/Config: add canonical `--strict-json` parsing for `config set` and keep `--json` as a legacy alias to reduce help/behavior drift. (#21332) thanks @adhitShet.
2122

2223
- Discord/Gateway: handle close code 4014 (missing privileged gateway intents) without crashing the gateway. Thanks @thewilloftheshadow.
2324
- Security/Net: strip sensitive headers (`Authorization`, `Proxy-Authorization`, `Cookie`, `Cookie2`) on cross-origin redirects in `fetchWithSsrFGuard` to prevent credential forwarding across origin boundaries. (#20313) Thanks @afurm.

docs/cli/config.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ openclaw config set agents.list[1].tools.exec.node "node-id-or-name"
3939
## Values
4040

4141
Values are parsed as JSON5 when possible; otherwise they are treated as strings.
42-
Use `--json` to require JSON5 parsing.
42+
Use `--strict-json` to require JSON5 parsing. `--json` remains supported as a legacy alias.
4343

4444
```bash
4545
openclaw config set agents.defaults.heartbeat.every "0m"
46-
openclaw config set gateway.port 19001 --json
47-
openclaw config set channels.whatsapp.groups '["*"]' --json
46+
openclaw config set gateway.port 19001 --strict-json
47+
openclaw config set channels.whatsapp.groups '["*"]' --strict-json
4848
```
4949

5050
Restart the gateway after edits.

src/cli/config-cli.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,51 @@ describe("config cli", () => {
135135
});
136136
});
137137

138+
describe("config set parsing flags", () => {
139+
it("falls back to raw string when parsing fails and strict mode is off", async () => {
140+
const resolved: OpenClawConfig = { gateway: { port: 18789 } };
141+
setSnapshot(resolved, resolved);
142+
143+
await runConfigCommand(["config", "set", "gateway.auth.mode", "{bad"]);
144+
145+
expect(mockWriteConfigFile).toHaveBeenCalledTimes(1);
146+
const written = mockWriteConfigFile.mock.calls[0]?.[0];
147+
expect(written.gateway?.auth).toEqual({ mode: "{bad" });
148+
});
149+
150+
it("throws when strict parsing is enabled via --strict-json", async () => {
151+
await expect(
152+
runConfigCommand(["config", "set", "gateway.auth.mode", "{bad", "--strict-json"]),
153+
).rejects.toThrow("__exit__:1");
154+
155+
expect(mockWriteConfigFile).not.toHaveBeenCalled();
156+
expect(mockReadConfigFileSnapshot).not.toHaveBeenCalled();
157+
});
158+
159+
it("keeps --json as a strict parsing alias", async () => {
160+
await expect(
161+
runConfigCommand(["config", "set", "gateway.auth.mode", "{bad", "--json"]),
162+
).rejects.toThrow("__exit__:1");
163+
164+
expect(mockWriteConfigFile).not.toHaveBeenCalled();
165+
expect(mockReadConfigFileSnapshot).not.toHaveBeenCalled();
166+
});
167+
168+
it("shows --strict-json and keeps --json as a legacy alias in help", async () => {
169+
const { registerConfigCli } = await import("./config-cli.js");
170+
const program = new Command();
171+
registerConfigCli(program);
172+
173+
const configCommand = program.commands.find((command) => command.name() === "config");
174+
const setCommand = configCommand?.commands.find((command) => command.name() === "set");
175+
const helpText = setCommand?.helpInformation() ?? "";
176+
177+
expect(helpText).toContain("--strict-json");
178+
expect(helpText).toContain("--json");
179+
expect(helpText).toContain("Legacy alias for --strict-json");
180+
});
181+
});
182+
138183
describe("config unset - issue #6070", () => {
139184
it("preserves existing config keys when unsetting a value", async () => {
140185
const resolved: OpenClawConfig = {

src/cli/config-cli.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { shortenHomePath } from "../utils.js";
1010
import { formatCliCommand } from "./command-format.js";
1111

1212
type PathSegment = string;
13+
type ConfigSetParseOpts = {
14+
strictJson?: boolean;
15+
};
1316

1417
function isIndexSegment(raw: string): boolean {
1518
return /^[0-9]+$/.test(raw);
@@ -67,9 +70,9 @@ function parsePath(raw: string): PathSegment[] {
6770
return parts.map((part) => part.trim()).filter(Boolean);
6871
}
6972

70-
function parseValue(raw: string, opts: { json?: boolean }): unknown {
73+
function parseValue(raw: string, opts: ConfigSetParseOpts): unknown {
7174
const trimmed = raw.trim();
72-
if (opts.json) {
75+
if (opts.strictJson) {
7376
try {
7477
return JSON5.parse(trimmed);
7578
} catch (err) {
@@ -313,14 +316,17 @@ export function registerConfigCli(program: Command) {
313316
.description("Set a config value by dot path")
314317
.argument("<path>", "Config path (dot or bracket notation)")
315318
.argument("<value>", "Value (JSON5 or raw string)")
316-
.option("--json", "Strict JSON5 parsing (error instead of raw string fallback)", false)
319+
.option("--strict-json", "Strict JSON5 parsing (error instead of raw string fallback)", false)
320+
.option("--json", "Legacy alias for --strict-json", false)
317321
.action(async (path: string, value: string, opts) => {
318322
try {
319323
const parsedPath = parsePath(path);
320324
if (parsedPath.length === 0) {
321325
throw new Error("Path is empty.");
322326
}
323-
const parsedValue = parseValue(value, opts);
327+
const parsedValue = parseValue(value, {
328+
strictJson: Boolean(opts.strictJson || opts.json),
329+
});
324330
const snapshot = await loadValidConfig();
325331
// Use snapshot.resolved (config after $include and ${ENV} resolution, but BEFORE runtime defaults)
326332
// instead of snapshot.config (runtime-merged with defaults).

0 commit comments

Comments
 (0)