Skip to content

fix(cli): route logs to stderr when --json flag is active#52102

Closed
artwalker wants to merge 3 commits intoopenclaw:mainfrom
artwalker:fix/json-cli-stdout-pollution
Closed

fix(cli): route logs to stderr when --json flag is active#52102
artwalker wants to merge 3 commits intoopenclaw:mainfrom
artwalker:fix/json-cli-stdout-pollution

Conversation

@artwalker
Copy link
Copy Markdown
Contributor

Summary

  • Call routeLogsToStderr() in the CLI preAction hook when --json mode is detected, before plugin loading
  • Plugin loading emits console.log messages that corrupt stdout JSON output for commands like openclaw agents list --json
  • Follows the existing pattern in src/cli/completion-cli.ts

Root Cause

src/cli/program/preaction.ts detects --json mode via isJsonOutputMode() but only uses it to suppress doctor output. It never calls routeLogsToStderr(), so plugin loading logs pollute stdout.

Test plan

  • npm run build passes
  • openclaw agents list --json | jq . parses cleanly without plugin log pollution

Fixes #52032

🤖 Generated with Claude Code

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]>
@openclaw-barnacle openclaw-barnacle bot added cli CLI command changes size: XS labels Mar 22, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 22, 2026

Greptile Summary

This PR fixes stdout pollution for JSON-mode CLI commands (e.g. openclaw agents list --json) by calling routeLogsToStderr() in the preAction hook as soon as --json mode is detected, before config guard and plugin loading run. The root cause was that isJsonOutputMode() was already being checked in preaction.ts but the result was only used to suppress doctor output — the broader log-rerouting step was missing, allowing plugin-loading console.log calls to corrupt the stdout JSON stream.

  • Fix is correctly placed: routeLogsToStderr() is called before both loadConfigGuardModule() and the fire-and-forget ensurePluginRegistryLoaded(), so all logs emitted during those operations are redirected to stderr.
  • Consistent with existing patterns: Follows the same approach used in src/cli/completion-cli.ts.
  • Commands that bypass config guard (backup, doctor, completion, secrets) are unaffected; completion already calls routeLogsToStderr() in its own action handler.
  • Minor style concern: The suppressDoctorStdout variable is reused to gate routeLogsToStderr(), but its name only describes one of its two effects. Renaming it to isJsonMode would improve readability.

Confidence Score: 5/5

  • This PR is safe to merge — it is a minimal, well-targeted fix with no logic regressions.
  • The change is a single-line addition that follows an established pattern already used in completion-cli.ts. The placement before plugin loading is correct, routeLogsToStderr() is idempotent and process-scoped, and the only concern is a non-blocking variable naming style issue.
  • No files require special attention.
Prompt To Fix All 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.

Last reviewed commit: "fix(cli): route logs..."

Comment on lines +147 to +150
const suppressDoctorStdout = isJsonOutputMode(commandPath, argv);
if (suppressDoctorStdout) {
routeLogsToStderr();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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:

Suggested change
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!

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +148 to +149
if (suppressDoctorStdout) {
routeLogsToStderr();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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]>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +163 to +166
const { ensurePluginRegistryLoaded } = await loadPluginRegistryModule();
ensurePluginRegistryLoaded({ scope: resolvePluginRegistryScope(commandPath) });
if (isJsonMode) {
restoreLogsToStdout();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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]>
@artwalker
Copy link
Copy Markdown
Contributor Author

Closing — fixed in v2026.3.23 upstream. Thanks!

@artwalker artwalker closed this Mar 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli CLI command changes size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: --json CLI flags output plugin logs to stdout, breaking JSON parsing

1 participant