Hmbown/CodeWhale · error · ExecError

ssh : agent returned no JSON receipt

Error message

ssh ${userHost}: agent returned no JSON receipt

What it means

After a successful (exit 0) ssh run, the transport filters stdout lines for ones starting with '{' and takes the last as the agent's JSON reply. If no such line exists (reply is null) it throws this ExecError: the agent produced no JSON receipt. The comment notes anything before the JSON line is MOTD noise, so this means the agent itself printed nothing parseable.

Solutions

  1. Re-run installRemoteAgent() to re-upload agent.mjs and backends
  2. Manually run ssh <userHost> 'node .codewhale-cu/agent/agent.mjs' to see raw stdout/stderr
  3. Check whether the remote shell prints banners that swallow the run; ensure agent writes its receipt to stdout
  4. Verify the remote file is not empty: ssh <userHost> 'wc -c .codewhale-cu/agent/agent.mjs'
  5. Confirm node version on remote supports the agent syntax

Example fix

// before
await ex.remote({ tool: "platform" }); // fails: no JSON line
// after
const r = await run("ssh", [...base, "cat", ".codewhale-cu/agent/agent.mjs"]); // verify agent exists
if (r.code !== 0 || r.stdout.trim() === "") await installRemoteAgent(ex, computer);
await ex.remote({ tool: "platform" });
Defensive patterns

Strategy: try-catch

Validate before calling

// verify agent file exists and is non-empty before remote calls
const chk = await run("ssh", [...base, "wc", "-c", ".codewhale-cu/agent/agent.mjs"]);
if (chk.code !== 0 || parseInt(chk.stdout) === 0) await installRemoteAgent(ex, computer);

Type guard

function isNoReceiptError(e) { return e instanceof ExecError && e.message.includes("no JSON receipt"); }

Try / catch

try {
  const reply = await ex.remote(request, opts);
} catch (e) {
  if (e.message.includes("no JSON receipt")) {
    await installRemoteAgent(ex, computer); // reinstall then retry once
    return ex.remote(request, opts);
  }
  throw e;
}

Prevention

When it happens

Trigger: executor.remote() with r.code===0 but no stdout line starting with '{': remote agent crashed silently, printed only MOTD/banner text, wrong script executed (e.g. agent path points at a non-agent file), or the agent exited 0 without printing a receipt.

Common situations: Agent script not fully installed/truncated; remote shell profile emits banners and the agent failed silently; the wrong file uploaded to .codewhale-cu/agent/agent.mjs; node executed a stub or empty file; ssh to a host where the marker dir is empty.

Related errors


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

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/transport.mjs:227

    kind: "ssh",
    base,
    userHost,
    remoteAgent,
    run(cmd, args = [], opts = {}) {
      // Local side commands (e.g. ssh itself) run directly.
      return run(cmd, args, opts);
    },
    async remote(request, opts = {}) {
      const r = await run("ssh", [...base, "node", remoteAgent, b64({ args: request.args ?? {}, tool: request.tool, nonce: crypto.randomBytes(6).toString("hex") })], {
        timeoutMs: opts.timeoutMs ?? 25_000,
      });
      if (r.aborted) throw Object.assign(new ExecError("computer request cancelled", r), { code: "cancelled" });
      if (r.timedOut) throw new ExecError(`ssh ${userHost}: timed out`, r);
      if (r.code !== 0) throw new ExecError(`ssh ${userHost} exited ${r.code}: ${r.stderr.trim().slice(0, 400)}`, r);
      // The agent prints exactly one JSON line; anything before it is MOTD noise.
      const line = r.stdout.trim().split("\n").filter((l) => l.startsWith("{")).pop();
      const reply = line ? JSON.parse(line) : null;
      if (!reply) throw new ExecError(`ssh ${userHost}: agent returned no JSON receipt`, r);
      return reply;
    },
  };
  attachPersistentChannel(ex, binding, ["ssh", ...base, "node", remoteAgent, "--serve"]);
  return ex;
}

/**
 * docker executor: a spawned task-owned desktop container. Same agent contract
 * as ssh, but the channel is `docker exec` — no sshd, no keys, the container
 * boundary itself is the isolation. Every call goes through
 * docker/agent-exec.sh, which joins the desktop session env (display + bus)
 * the container entrypoint recorded before serving requests.
 */
export function dockerExec(computer, binding) {
  const container = safeRemotePath(computer.container);
  const remoteAgent = "/app/docker/agent-exec.sh";
  const ex = {

View on GitHub (pinned to 73e0f67d83)