Skip to content

fix: route logs to stderr in --json output mode#52063

Closed
xl0shk wants to merge 2 commits intoopenclaw:mainfrom
xl0shk:fix/json-stdout-logs
Closed

fix: route logs to stderr in --json output mode#52063
xl0shk wants to merge 2 commits intoopenclaw:mainfrom
xl0shk:fix/json-stdout-logs

Conversation

@xl0shk
Copy link
Copy Markdown

@xl0shk xl0shk commented Mar 22, 2026

Fixes #52032

Summary

  • Call routeLogsToStderr() in both CLI execution paths (fast-path routes and Commander preAction hook) when --json is detected, so all log output goes to stderr and stdout stays clean for JSON parsing.

Root Cause

The CLI has two execution paths that both need the fix:

  1. Fast-path (src/cli/route.ts): prepareRoutedCommand() detected --json but only used it for suppressDoctorStdout, never called routeLogsToStderr().
  2. Commander path (src/cli/program/preaction.ts): Same issue — detected --json via isJsonOutputMode() but didn't call routeLogsToStderr().

The existing routeLogsToStderr() function (already used in completion-cli.ts for the same reason) simply sets loggingState.forceConsoleToStderr = true, which the subsystem logger's writeConsoleLine() checks to route output to stderr.

Changes

  • src/cli/route.ts: call routeLogsToStderr() in prepareRoutedCommand() when --json flag is present
  • src/cli/program/preaction.ts: call routeLogsToStderr() in the preAction hook when isJsonOutputMode() returns true
  • Added tests in src/cli/route.test.ts and src/cli/program/preaction.test.ts

Test plan

  • pnpm test -- src/cli/route.test.ts src/cli/program/preaction.test.ts — 15/15 pass
  • pnpm check — all pass

This PR was developed with AI assistance (Claude Code).

@openclaw-barnacle openclaw-barnacle Bot added cli CLI command changes size: S labels Mar 22, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 22, 2026

Greptile Summary

This PR fixes a bug where log output could leak to stdout when using --json mode, breaking JSON parsing for callers. The root cause was that routeLogsToStderr() was never invoked in either CLI execution path — the fast-path (src/cli/route.ts) or the Commander preAction hook (src/cli/program/preaction.ts) — even though --json was detected.

Changes:

  • src/cli/route.ts: prepareRoutedCommand() now calls routeLogsToStderr() when hasFlag(argv, "--json") is true, matching the existing suppressDoctorStdout logic.
  • src/cli/program/preaction.ts: The preAction hook now calls routeLogsToStderr() when isJsonOutputMode() returns true. The isJsonOutputMode() function already correctly excludes parse-only commands (e.g., config set) via JSON_PARSE_ONLY_COMMANDS, so those commands are unaffected.
  • Both changed files have corresponding new tests covering the happy path, the non-json path, and the parse-only exclusion.

The fix is minimal and targeted, reusing the existing routeLogsToStderr() function (already used in completion-cli.ts for the same reason) without introducing new state or abstractions.

Confidence Score: 5/5

  • This PR is safe to merge — it applies a focused, well-tested fix to both CLI execution paths with no behavioral regressions.
  • The change is minimal (three lines of production code across two files), directly mirrors the existing completion-cli.ts pattern, and is guarded by the same condition already used for suppressDoctorStdout. Tests cover all relevant cases including the config set parse-only exclusion. No new state, no new abstractions, no side effects beyond the intended stderr routing.
  • No files require special attention.

Last reviewed commit: "CLI: route logs to s..."

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: 959d66c566

ℹ️ 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 147 to +150
const suppressDoctorStdout = isJsonOutputMode(commandPath, argv);
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 Keep Commander --json command output on stdout

runCli() enables console capture before the Commander path is built (src/cli/run-main.ts:113-116), so calling routeLogsToStderr() here flips every later console.log/runtime.log write onto stderr. Many non-routed JSON commands emit their actual machine-readable payload through runtime.log, for example src/commands/channels/list.ts:122-129. After this change, openclaw channels list --json will leave stdout empty and put the JSON on stderr, which breaks any caller that parses stdout.

Useful? React with 👍 / 👎.

Comment thread src/cli/route.ts
Comment on lines 14 to +17
const suppressDoctorStdout = hasFlag(params.argv, "--json");
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.

P2 Badge Apply stderr routing before the fast path executes

In the routed path this call only sets loggingState.forceConsoleToStderr, but runCli() does not call enableConsoleCapture() until after tryRouteCli() has already been skipped (src/cli/run-main.ts:109-114). That means routed commands still use the native console.log/defaultRuntime.log, so textual diagnostics continue to leak to stdout. A concrete case is memory status --json: withMemoryManagerForAgent() can emit defaultRuntime.log(error ?? "Memory search disabled.") in src/cli/memory-cli.ts:138 before the JSON payload at line 425, so stdout is still not parseable JSON in that scenario.

Useful? React with 👍 / 👎.

@xl0shk
Copy link
Copy Markdown
Author

xl0shk commented Mar 22, 2026

The two CI failures are unrelated to this change:

  1. channels — Flaky discord test (monitor.tool-result.sends-status-replies-responseprefix.test.ts) failing on vi.waitFor() timing. This test is in extensions/discord/, untouched by this PR.
  2. install-smoke — Install smoke test, also unrelated to CLI log routing.

All other 30+ checks pass, including the directly relevant check, test (unit shards 1+2), extensions, contracts, build-smoke, and all Windows test shards.

Requesting a re-run of the failed jobs.

@steipete
Copy link
Copy Markdown
Contributor

Closing this as implemented after Codex review.

Current main already implements this fix via the safer landed path from #52449, and that implementation shipped in v2026.3.23. The open PR’s global stderr redirect approach was superseded.

What I checked:

  • Landed replacement PR: GitHub API shows PR fix(cli): route plugin logs to stderr during --json output #52449 merged on 2026-03-23 with merge commit 97e4f371713ea573a1ba6c6db5f9a7fabb472362 and message fix: keep status --json stdout clean (#52449). (97e4f371713e)
  • Shipped in release: GitHub compare shows tag v2026.3.23 is ahead of merge commit 97e4f371713ea573a1ba6c6db5f9a7fabb472362, so that fix was included in the v2026.3.23 release. (97e4f371713e)
  • JSON payloads stay on stdout: defaultRuntime.writeJson() writes directly to process.stdout.write, and writeRuntimeJson() prefers that path when available, so machine-readable JSON is not forced through console.log/stderr routing. (src/runtime.ts:82, 60f7a59f5ed5)
  • Plugin-load logs are scoped to stderr only during bootstrap: Current bootstrap passes suppressDoctorStdout into plugin loading, and ensureCliPluginRegistryLoaded() temporarily flips loggingState.forceConsoleToStderr only around ensurePluginRegistryLoaded() before restoring the previous value. (src/cli/plugin-registry-loader.ts:18, 60f7a59f5ed5)
  • Current tests cover both commander and routed paths: preaction.test.ts asserts stderr routing during plugin loading for agents list --json and restoration afterward, and route.test.ts asserts the same for routed commands. status.scan.fast-json.test.ts also verifies the lean status --json path skips plugin loading entirely. (src/cli/program/preaction.test.ts:459, 60f7a59f5ed5)

So I’m closing this as already implemented rather than keeping a duplicate issue open.

Review notes: reviewed against 60f7a59f5ed5; fix evidence: release v2026.3.23, commit 97e4f371713e.

@steipete steipete closed this Apr 25, 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

2 participants