Hmbown/CodeWhale · error · ExecError

powershell did not return JSON

Error message

powershell did not return JSON: ${(r.stderr || out).trim().slice(0, 300)}

What it means

psJson runs a script through psOk (which guarantees a zero exit code) and then parses stdout as JSON via tryJson. If the trimmed stdout is not valid JSON, it throws this ExecError including up to 300 chars of stderr or stdout to show what actually came back. This catches scripts that succeeded but printed unexpected output.

Solutions

  1. Inspect the 300-char excerpt in the error to see what polluted stdout
  2. In your script, redirect all non-JSON output to stderr (Write-Host/Write-Warning to stderr) or suppress profile loading (-NoProfile)
  3. Ensure a single ConvertTo-Json -Compress -Depth N call is the only stdout output and use Out-String where needed
  4. Force UTF-8 output encoding in the script: [Console]::OutputEncoding = [Text.Encoding]::UTF8

Example fix

// before
# powershell
"loading..."
ConvertTo-Json $result
// after
# powershell
Write-Warning "loading..."   # goes to stderr
ConvertTo-Json $result -Compress -Depth 6
Defensive patterns

Strategy: validation

Type guard

function isJsonObject(s) { try { const v = JSON.parse(s); return v && typeof v === 'object'; } catch { return false; } }

Try / catch

try { return await psJson(script); }
catch (e) {
  if (/did not return JSON/.test(e.message)) log.error('stdout polluted:', e.message);
  throw e;
}

Prevention

When it happens

Trigger: A psJson-backed script prints progress/warning lines before the JSON, emits nothing on stdout, uses ConvertTo-Json without -Compress in a way that mangles output, or writes diagnostics to stdout while stderr is empty.

Common situations: Profile scripts ($PROFILE) or modules echoing banners on load; ConvertTo-Json emitting multiple documents for arrays; BOM or console encoding (e.g. UTF-16) corrupting the first bytes; Windows PowerShell 5.1 vs PowerShell 7 differences in JSON formatting.

Understand the failure class

Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/0fb22515f209cf28. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/backends/win32.mjs:199

    if (r.aborted) throw Object.assign(new ExecError("computer request cancelled", r), { code: "cancelled" });
    if (r.timedOut) throw new ExecError(`powershell timed out after ${o.timeoutMs ?? 25_000}ms`, r);
    if (r.code !== 0) {
      const raw = (r.stderr || r.stdout).trim();
      // EncodedCommand serializes errors as CLIXML; surface the error strings,
      // not a truncated XML/progress header that conceals the actual failure.
      const messages = [...raw.matchAll(/<S S="Error">([\s\S]*?)<\/S>/g)].map(m => m[1]
        .replace(/_x([0-9A-Fa-f]{4})_/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
        .replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&"));
      throw new ExecError(`powershell.exe exited ${r.code}: ${(messages.join("") || raw).slice(0, 1600)}`, r);
    }
    return r;
  }

  async function psJson(script, o = {}) {
    const r = await psOk(script, o);
    const out = r.stdout.trim();
    const j = tryJson(out, null);
    if (!j) throw new ExecError(`powershell did not return JSON: ${(r.stderr || out).trim().slice(0, 300)}`, r);
    return j;
  }

  let lastRaster = null;
  let activeDisplay = null;
  const heldButtons = new Set();
  const heldKeys = new Set();

  async function releaseInput({ buttons = [...heldButtons], keys = [...heldKeys] } = {}) {
    buttons = buttons.filter((button) => heldButtons.has(button));
    keys = keys.filter((key) => heldKeys.has(key));
    if (!buttons.length && !keys.length) return;
    const releases = [
      ...buttons.map((button) => `[User32]::mouse_event([User32]::${button}UP, 0, 0, 0, [UIntPtr]::Zero);`),
      ...keys.reverse().map((vk) => `[User32]::SendKey(${vk}, 2);`),
    ];
    await withSignal(null, () => withUser32(releases.join("\n"), { timeoutMs: 2_000 }));
    for (const button of buttons) heldButtons.delete(button);

View on GitHub (pinned to 73e0f67d83)