openclaw - 💡(How to fix) Fix [Bug]: `openclaw commitments [list|dismiss] --json` produces empty stdout — all JSON output goes to stderr, exit 0 (automation breaker; silent `… | jq` failures)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

openclaw commitments --json, openclaw commitments list --json, openclaw commitments list --all --json, and openclaw commitments dismiss <id> --json all produce empty stdout (0 bytes) and emit the full JSON payload to stderr instead, while exiting 0. Any shell pipeline like openclaw commitments list --json | jq . parses empty input and fails silently (jq exits 4 on empty input by default; downstream jq -e/jq ./grep etc. each fail their own way).

Root cause is in src/commands/commitments.ts:107 and src/commands/commitments.ts:162: the JSON branch calls runtime.log(JSON.stringify(...)). The repo-wide console patch at src/logging/console.ts:263-268 redirects ALL console.log calls to process.stderr.write(...) when loggingState.forceConsoleToStderr is true — which is exactly what --json mode toggles (src/logging/console.ts:105, src/cli/plugin-registry-loader.ts:25).

// src/logging/console.ts:263-274
if (loggingState.forceConsoleToStderr) {
  // In --json mode, all console.* writes are diagnostics and should stay off stdout.
  try {
    const redacted = redactSensitiveText(formatted);
    const line = timestamp ? `${timestamp} ${redacted}` : redacted;
    process.stderr.write(`${line}\n`);   // ← commitments' JSON payload lands here
  } catch (err) {}
}

That forceConsoleToStderr redirect is correct policy for diagnostic console.log calls during --json mode. The bug is that the commitments command is also emitting its machine-readable JSON output through the same runtime.logconsole.log path, so its real JSON output gets caught by the same redirect.

Working sibling commands (agents list --json, cron list --json, tasks list --json, etc.) use writeRuntimeJson(runtime, value) — defined at src/runtime.ts:106-116 — which bypasses console.log and writes directly to process.stdout.write. See src/commands/agents.commands.list.ts:137:

writeRuntimeJson(runtime, summaries);  // ← writes directly to stdout, bypasses console patch

Verified live on v2026.5.10-beta.1 (152ea9af34); same source on v2026.5.7 stable (the runtime.log(JSON.stringify(...)) pattern is identical there).

Error Message

← silently empty; jq exits 4 on parse error

Root Cause

Root cause is in src/commands/commitments.ts:107 and src/commands/commitments.ts:162: the JSON branch calls runtime.log(JSON.stringify(...)). The repo-wide console patch at src/logging/console.ts:263-268 redirects ALL console.log calls to process.stderr.write(...) when loggingState.forceConsoleToStderr is true — which is exactly what --json mode toggles (src/logging/console.ts:105, src/cli/plugin-registry-loader.ts:25).

Fix Action

Fix / Workaround

Root cause is in src/commands/commitments.ts:107 and src/commands/commitments.ts:162: the JSON branch calls runtime.log(JSON.stringify(...)). The repo-wide console patch at src/logging/console.ts:263-268 redirects ALL console.log calls to process.stderr.write(...) when loggingState.forceConsoleToStderr is true — which is exactly what --json mode toggles (src/logging/console.ts:105, src/cli/plugin-registry-loader.ts:25).

writeRuntimeJson(runtime, summaries);  // ← writes directly to stdout, bypasses console patch

Code Example

// src/logging/console.ts:263-274
if (loggingState.forceConsoleToStderr) {
  // In --json mode, all console.* writes are diagnostics and should stay off stdout.
  try {
    const redacted = redactSensitiveText(formatted);
    const line = timestamp ? `${timestamp} ${redacted}` : redacted;
    process.stderr.write(`${line}\n`);   // ← commitments' JSON payload lands here
  } catch (err) {}
}

---

writeRuntimeJson(runtime, summaries);  // ← writes directly to stdout, bypasses console patch

---

cd ~/Gittensor/OpenCoven/openclaw-upstream         # any current checkout
export PATH=~/.nvm/versions/node/v22.22.2/bin:$PATH

# Seed one commitment to exercise dismiss too
mkdir -p ~/.openclaw/commitments
node -e '
  const fs=require("fs"),os=require("os"),p=require("path");
  const now=Date.now();
  fs.writeFileSync(p.join(os.homedir(),".openclaw/commitments/commitments.json"),
    JSON.stringify({version:1,commitments:[{id:"cm_evi",agentId:"qa",sessionKey:"qa",channel:"slack",kind:"follow_up",sensitivity:"normal",source:"qa",status:"pending",reason:"qa",suggestedText:"qa",dedupeKey:"qa-evi",confidence:0.9,dueWindow:{earliestMs:now-1000,latestMs:now+3600000,timezone:"UTC"},createdAtMs:now,updatedAtMs:now,attempts:0}]},null,2)+"\n",{mode:0o600});
'

for inv in "commitments --json" \
           "commitments list --json" \
           "commitments list --all --json" \
           "commitments dismiss cm_evi --json"; do
  echo "=== $inv ==="
  pnpm openclaw $inv 1>/tmp/cmout 2>/tmp/cmerr
  echo "exit=$? stdout=$(wc -c </tmp/cmout)b stderr=$(wc -c </tmp/cmerr)b"
  jq -e . </tmp/cmout >/dev/null 2>&1 && echo "  ✓ stdout JSON parseable" || echo "  ✗ stdout NOT parseable (empty)"
done

---

$ pnpm openclaw commitments list --json 1>/tmp/o 2>/tmp/e
$ echo "exit=$? stdout=$(wc -c </tmp/o)b stderr=$(wc -c </tmp/e)b"
exit=0 stdout=0b stderr=693b

$ cat /tmp/o                           # ← empty
$ cat /tmp/e                           # ← the JSON is here, on stderr
{
  "count": 1,
  "status": "pending",
  "agentId": null,
  "store": "/home/orin/.openclaw/commitments/commitments.json",
  "commitments": []
}

---

$ pnpm openclaw commitments list --json | jq .
                                              # ← silently empty; jq exits 4 on parse error

---

- import type { RuntimeEnv } from "../runtime.js";
+ import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
  if (opts.json) {
-   runtime.log(
-     JSON.stringify(
-       {
-         count: commitments.length,
-         status: status ?? (opts.all ? null : "pending"),
-         agentId: normalizeOptionalString(opts.agent) ?? null,
-         store: resolveCommitmentStorePath(),
-         commitments,
-       },
-       null,
-       2,
-     ),
-   );
+   writeRuntimeJson(runtime, {
+     count: commitments.length,
+     status: status ?? (opts.all ? null : "pending"),
+     agentId: normalizeOptionalString(opts.agent) ?? null,
+     store: resolveCommitmentStorePath(),
+     commitments,
+   });
    return;
  }
  if (opts.json) {
-   runtime.log(JSON.stringify({ dismissed: ids }, null, 2));
+   writeRuntimeJson(runtime, { dismissed: ids });
    return;
  }

---
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

openclaw commitments --json, openclaw commitments list --json, openclaw commitments list --all --json, and openclaw commitments dismiss <id> --json all produce empty stdout (0 bytes) and emit the full JSON payload to stderr instead, while exiting 0. Any shell pipeline like openclaw commitments list --json | jq . parses empty input and fails silently (jq exits 4 on empty input by default; downstream jq -e/jq ./grep etc. each fail their own way).

Root cause is in src/commands/commitments.ts:107 and src/commands/commitments.ts:162: the JSON branch calls runtime.log(JSON.stringify(...)). The repo-wide console patch at src/logging/console.ts:263-268 redirects ALL console.log calls to process.stderr.write(...) when loggingState.forceConsoleToStderr is true — which is exactly what --json mode toggles (src/logging/console.ts:105, src/cli/plugin-registry-loader.ts:25).

// src/logging/console.ts:263-274
if (loggingState.forceConsoleToStderr) {
  // In --json mode, all console.* writes are diagnostics and should stay off stdout.
  try {
    const redacted = redactSensitiveText(formatted);
    const line = timestamp ? `${timestamp} ${redacted}` : redacted;
    process.stderr.write(`${line}\n`);   // ← commitments' JSON payload lands here
  } catch (err) {}
}

That forceConsoleToStderr redirect is correct policy for diagnostic console.log calls during --json mode. The bug is that the commitments command is also emitting its machine-readable JSON output through the same runtime.logconsole.log path, so its real JSON output gets caught by the same redirect.

Working sibling commands (agents list --json, cron list --json, tasks list --json, etc.) use writeRuntimeJson(runtime, value) — defined at src/runtime.ts:106-116 — which bypasses console.log and writes directly to process.stdout.write. See src/commands/agents.commands.list.ts:137:

writeRuntimeJson(runtime, summaries);  // ← writes directly to stdout, bypasses console patch

Verified live on v2026.5.10-beta.1 (152ea9af34); same source on v2026.5.7 stable (the runtime.log(JSON.stringify(...)) pattern is identical there).

Steps to reproduce

cd ~/Gittensor/OpenCoven/openclaw-upstream         # any current checkout
export PATH=~/.nvm/versions/node/v22.22.2/bin:$PATH

# Seed one commitment to exercise dismiss too
mkdir -p ~/.openclaw/commitments
node -e '
  const fs=require("fs"),os=require("os"),p=require("path");
  const now=Date.now();
  fs.writeFileSync(p.join(os.homedir(),".openclaw/commitments/commitments.json"),
    JSON.stringify({version:1,commitments:[{id:"cm_evi",agentId:"qa",sessionKey:"qa",channel:"slack",kind:"follow_up",sensitivity:"normal",source:"qa",status:"pending",reason:"qa",suggestedText:"qa",dedupeKey:"qa-evi",confidence:0.9,dueWindow:{earliestMs:now-1000,latestMs:now+3600000,timezone:"UTC"},createdAtMs:now,updatedAtMs:now,attempts:0}]},null,2)+"\n",{mode:0o600});
'

for inv in "commitments --json" \
           "commitments list --json" \
           "commitments list --all --json" \
           "commitments dismiss cm_evi --json"; do
  echo "=== $inv ==="
  pnpm openclaw $inv 1>/tmp/cmout 2>/tmp/cmerr
  echo "exit=$? stdout=$(wc -c </tmp/cmout)b stderr=$(wc -c </tmp/cmerr)b"
  jq -e . </tmp/cmout >/dev/null 2>&1 && echo "  ✓ stdout JSON parseable" || echo "  ✗ stdout NOT parseable (empty)"
done

Result on v2026.5.10-beta.1 (152ea9af34): all four invocations exit 0 with 0 bytes on stdout; the full JSON payload (693 / 693 / 688 / 43 bytes respectively) goes to stderr.

Expected behavior

--json output should land on stdout so it's pipeable to jq, python -m json.tool, etc. — matching how agents list --json, cron list --json, tasks list --json, skills list --json, sessions --json, devices list --json, channels list --json, approvals get --json, etc. all behave.

Diagnostics (logger output, deprecation notices, sensitive-text redactions, etc.) correctly stay on stderr in JSON mode (that's what forceConsoleToStderr is for). The bug is only that commitments' primary JSON output is misrouted as if it were a diagnostic.

Actual behavior

Verbatim on v2026.5.10-beta.1:

$ pnpm openclaw commitments list --json 1>/tmp/o 2>/tmp/e
$ echo "exit=$? stdout=$(wc -c </tmp/o)b stderr=$(wc -c </tmp/e)b"
exit=0 stdout=0b stderr=693b

$ cat /tmp/o                           # ← empty
$ cat /tmp/e                           # ← the JSON is here, on stderr
{
  "count": 1,
  "status": "pending",
  "agentId": null,
  "store": "/home/orin/.openclaw/commitments/commitments.json",
  "commitments": [ … ]
}
$ pnpm openclaw commitments list --json | jq .
                                              # ← silently empty; jq exits 4 on parse error

For contrast, the working sibling agents list --json puts 314 bytes on stdout, 0 on stderr, and jq . succeeds.

Source diff that fixes it (in src/commands/commitments.ts):

- import type { RuntimeEnv } from "../runtime.js";
+ import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
   if (opts.json) {
-   runtime.log(
-     JSON.stringify(
-       {
-         count: commitments.length,
-         status: status ?? (opts.all ? null : "pending"),
-         agentId: normalizeOptionalString(opts.agent) ?? null,
-         store: resolveCommitmentStorePath(),
-         commitments,
-       },
-       null,
-       2,
-     ),
-   );
+   writeRuntimeJson(runtime, {
+     count: commitments.length,
+     status: status ?? (opts.all ? null : "pending"),
+     agentId: normalizeOptionalString(opts.agent) ?? null,
+     store: resolveCommitmentStorePath(),
+     commitments,
+   });
    return;
  }
   if (opts.json) {
-   runtime.log(JSON.stringify({ dismissed: ids }, null, 2));
+   writeRuntimeJson(runtime, { dismissed: ids });
    return;
  }

Three call sites in src/commands/commitments.ts (lines 107-119 for list, line 162 for dismiss; plus the third list-style path the bare commitments action goes through is the same function). All three branches change from runtime.log(JSON.stringify(...)) to writeRuntimeJson(runtime, ...).

OpenClaw version

v2026.5.10-beta.1, v2026.5.7

Operating system

Ubuntu 24.04

Install method

No response

Model

Not applicable

Provider / routing chain

Not applicable

Additional provider/model setup details

No response

Logs, screenshots, and evidence

Impact and severity

No response

Additional information

No response

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

--json output should land on stdout so it's pipeable to jq, python -m json.tool, etc. — matching how agents list --json, cron list --json, tasks list --json, skills list --json, sessions --json, devices list --json, channels list --json, approvals get --json, etc. all behave.

Diagnostics (logger output, deprecation notices, sensitive-text redactions, etc.) correctly stay on stderr in JSON mode (that's what forceConsoleToStderr is for). The bug is only that commitments' primary JSON output is misrouted as if it were a diagnostic.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix [Bug]: `openclaw commitments [list|dismiss] --json` produces empty stdout — all JSON output goes to stderr, exit 0 (automation breaker; silent `… | jq` failures)