fix(cli): route logs to stderr when --json flag is active#52102
fix(cli): route logs to stderr when --json flag is active#52102artwalker wants to merge 3 commits intoopenclaw:mainfrom
Conversation
Plugin loading emits console.log messages to stdout, which corrupts JSON output for commands like `openclaw agents list --json`. Call routeLogsToStderr() in the preAction hook before plugin loading when --json mode is detected, matching the existing pattern in completion-cli. Fixes openclaw#52032 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Greptile SummaryThis PR fixes stdout pollution for JSON-mode CLI commands (e.g.
Confidence Score: 5/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/cli/program/preaction.ts
Line: 147-150
Comment:
**Variable name no longer reflects full purpose**
`suppressDoctorStdout` is now used to gate two distinct effects: suppressing doctor output *and* routing all logs to stderr for clean JSON output. The name only describes one of those effects, which may confuse future readers maintaining this code.
Consider renaming to something that captures the full intent, e.g. `isJsonMode`:
```suggestion
const isJsonMode = isJsonOutputMode(commandPath, argv);
if (isJsonMode) {
routeLogsToStderr();
}
```
And update the downstream spread on line 155 accordingly:
```
...(isJsonMode ? { suppressDoctorStdout: true } : {}),
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: "fix(cli): route logs..." |
src/cli/program/preaction.ts
Outdated
| const suppressDoctorStdout = isJsonOutputMode(commandPath, argv); | ||
| if (suppressDoctorStdout) { | ||
| routeLogsToStderr(); | ||
| } |
There was a problem hiding this comment.
Variable name no longer reflects full purpose
suppressDoctorStdout is now used to gate two distinct effects: suppressing doctor output and routing all logs to stderr for clean JSON output. The name only describes one of those effects, which may confuse future readers maintaining this code.
Consider renaming to something that captures the full intent, e.g. isJsonMode:
| const suppressDoctorStdout = isJsonOutputMode(commandPath, argv); | |
| if (suppressDoctorStdout) { | |
| routeLogsToStderr(); | |
| } | |
| const isJsonMode = isJsonOutputMode(commandPath, argv); | |
| if (isJsonMode) { | |
| routeLogsToStderr(); | |
| } |
And update the downstream spread on line 155 accordingly:
...(isJsonMode ? { suppressDoctorStdout: true } : {}),
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cli/program/preaction.ts
Line: 147-150
Comment:
**Variable name no longer reflects full purpose**
`suppressDoctorStdout` is now used to gate two distinct effects: suppressing doctor output *and* routing all logs to stderr for clean JSON output. The name only describes one of those effects, which may confuse future readers maintaining this code.
Consider renaming to something that captures the full intent, e.g. `isJsonMode`:
```suggestion
const isJsonMode = isJsonOutputMode(commandPath, argv);
if (isJsonMode) {
routeLogsToStderr();
}
```
And update the downstream spread on line 155 accordingly:
```
...(isJsonMode ? { suppressDoctorStdout: true } : {}),
```
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 167627ae49
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
src/cli/program/preaction.ts
Outdated
| if (suppressDoctorStdout) { | ||
| routeLogsToStderr(); |
There was a problem hiding this comment.
Avoid redirecting JSON command payloads to stderr
Calling routeLogsToStderr() for every --json invocation flips loggingState.forceConsoleToStderr, so all subsequent console.log writes go to stderr (src/logging/console.ts), not just startup/plugin noise. Many JSON command handlers emit their actual machine-readable payload through runtime.log(...) (for example channelsListCommand uses runtime.log(JSON.stringify(...)) in src/commands/channels/list.ts), and runtime.log delegates to console.log (src/runtime.ts). As a result, non-routed JSON commands like channels ... --json will produce empty stdout and break pipelines expecting JSON on stdout.
Useful? React with 👍 / 👎.
Address review feedback on the original routeLogsToStderr() fix: - P1: Global routeLogsToStderr() also redirected JSON command output (via runtime.log → console.log) to stderr, breaking --json pipelines. Now only active during plugin loading and restored immediately after. - P1: Apply the same protection to src/cli/route.ts which has a separate plugin loading path for fast-routed commands. - P2: Rename local variable from suppressDoctorStdout to isJsonMode in preaction.ts to reflect its expanded purpose. - Add restoreLogsToStdout() export to src/logging/console.ts. Fixes openclaw#52032 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 03ad143948
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
src/cli/program/preaction.ts
Outdated
| const { ensurePluginRegistryLoaded } = await loadPluginRegistryModule(); | ||
| ensurePluginRegistryLoaded({ scope: resolvePluginRegistryScope(commandPath) }); | ||
| if (isJsonMode) { | ||
| restoreLogsToStdout(); |
There was a problem hiding this comment.
Restore stdout routing in a finally block
When --json is active, routeLogsToStderr() is set before plugin loading, but restoreLogsToStdout() only runs on the success path. If ensurePluginRegistryLoaded throws (for example, a broken plugin manifest), loggingState.forceConsoleToStderr stays enabled for the rest of the process, so later runtime.log(...)/console.log(...) output from in-process callers (tests or embedded CLI invocations) is redirected to stderr and no longer appears on stdout as expected. Wrap the plugin-load section in try/finally so restoration is guaranteed.
Useful? React with 👍 / 👎.
Ensure restoreLogsToStdout() runs even if ensurePluginRegistryLoaded() throws, preventing permanent stderr redirection in embedded CLI invocations or test runners. Applied to both preaction.ts and route.ts plugin loading paths. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
|
Closing — fixed in v2026.3.23 upstream. Thanks! |
Summary
routeLogsToStderr()in the CLI preAction hook when--jsonmode is detected, before plugin loadingconsole.logmessages that corrupt stdout JSON output for commands likeopenclaw agents list --jsonsrc/cli/completion-cli.tsRoot Cause
src/cli/program/preaction.tsdetects--jsonmode viaisJsonOutputMode()but only uses it to suppress doctor output. It never callsrouteLogsToStderr(), so plugin loading logs pollute stdout.Test plan
npm run buildpassesopenclaw agents list --json | jq .parses cleanly without plugin log pollutionFixes #52032
🤖 Generated with Claude Code