Skip to content

CLI: route JSON preaction logs to stderr#52060

Open
08820048 wants to merge 1 commit intoopenclaw:mainfrom
08820048:fix-cli-json-stderr-52032
Open

CLI: route JSON preaction logs to stderr#52060
08820048 wants to merge 1 commit intoopenclaw:mainfrom
08820048:fix-cli-json-stderr-52032

Conversation

@08820048
Copy link
Copy Markdown

Summary

  • Problem: CLI commands with --json could still print plugin/bootstrap logs to stdout before the JSON payload.
  • Why it matters: this breaks machine-readable stdout contracts for commands like openclaw agents list --json | jq ....
  • What changed: the CLI preAction hook now routes logs to stderr for real JSON-output commands before config guard or plugin loading runs, and the regression test covers both enabled and excluded --json paths.
  • What did NOT change (scope boundary): no log levels changed, no command payload shapes changed, and parse-only --json aliases such as config set --json remain untouched.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

CLI commands that support real --json output now keep stdout reserved for JSON by routing preAction/plugin/bootstrap logs to stderr.

Security Impact (required)

  • New permissions/capabilities? (Yes/No) No
  • Secrets/tokens handling changed? (Yes/No) No
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) No
  • Data access scope changed? (Yes/No) No
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: macOS 26.4
  • Runtime/container: local zsh shell
  • Model/provider: N/A
  • Integration/channel (if any): CLI preAction
  • Relevant config (redacted): --json commands that can load plugins during startup

Steps

  1. Run a CLI command with real JSON output and plugin/bootstrap startup, such as openclaw agents list --json.
  2. Pipe stdout to a JSON parser.
  3. Confirm startup logs no longer corrupt stdout.

Expected

  • Stdout remains valid JSON for real --json output commands.

Actual

  • Before: plugin/bootstrap logs could print to stdout before the JSON payload.
  • After: those logs are redirected to stderr before config guard/plugin loading, preserving JSON stdout.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios: pnpm test -- src/cli/program/preaction.test.ts passes and now asserts status --json, update status --json, and backup create --json flip stderr routing, while config set --json does not.
  • Edge cases checked: JSON routing is applied before config-guard bypass, so bypassed JSON commands still preserve stdout.
  • What you did not verify: I did not reproduce a plugin-loaded end-to-end CLI run in this terminal; pnpm build also fails on current upstream/main due to unrelated untouched memory export errors in src/memory/embeddings-openai.ts and src/memory/manager-sync-ops.ts.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps:

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: revert commit f902d4989
  • Files/config to restore: src/cli/program/preaction.ts, src/cli/program/preaction.test.ts
  • Known bad symptoms reviewers should watch for: unexpected stderr routing on commands that only use --json as an input-parsing mode

Risks and Mitigations

  • Risk: a parse-only --json command could accidentally be treated as JSON output and change stderr behavior.
    • Mitigation: the change keeps the existing isJsonOutputMode(...) gate and regression coverage for config set --json.

@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 a bug where plugin/bootstrap initialization logs could leak to stdout before the JSON payload when running CLI commands with --json, breaking machine-readable stdout contracts for patterns like openclaw agents list --json | jq .... The fix adds a routeLogsToStderr() call in the preAction hook — importantly, before the shouldBypassConfigGuard() early-return — ensuring that even config-guard-bypassing commands (e.g. backup create --json) still get their logs redirected to stderr.

  • The change is minimal, targeted, and well-scoped: only two files touched, no log-level or payload-shape changes.
  • emitCliBanner was already guarded with its own hasJsonFlag check (returns early for --json), so the banner was never part of the pollution problem and remains unaffected.
  • Renaming the local suppressDoctorStdout variable to reuse the already-computed jsonOutputMode is a clean deduplication.
  • Test coverage is thorough: status --json, update status --json, and backup create --json all assert loggingState.forceConsoleToStderr === true; config set --json correctly asserts false. beforeEach/afterEach teardown and mid-test resets prevent state leakage between sub-scenarios.

Confidence Score: 5/5

  • This PR is safe to merge — the change is minimal, logically correct, and well-covered by regression tests.
  • The diff is two files, the logic change is a single if-block moved above an early-return guard, the pre-existing isJsonOutputMode gate is unchanged, and the banner's own hasJsonFlag guard already covers that edge case. All new and existing assertions pass. No API contracts, log levels, or command payload shapes are altered.
  • No files require special attention.

Last reviewed commit: "CLI: route JSON prea..."

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

ℹ️ 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 +145 to +146
if (jsonOutputMode) {
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 Do not redirect --json command payloads to stderr

Setting routeLogsToStderr() here flips global console routing before command execution, and in normal CLI execution (src/cli/run-main.ts) enableConsoleCapture() has already patched console.*. Since defaultRuntime.log uses console.log and many JSON-mode handlers emit their payload via runtime.log(JSON.stringify(...)) (for example in status/update/backup commands), the actual JSON result is moved to stderr rather than stdout, breaking pipelines like openclaw ... --json | jq that expect machine-readable stdout.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli CLI command changes size: XS

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