Hmbown/CodeWhale · error · ExecError

ssh exited

Error message

ssh ${userHost} exited ${r.code}: ${r.stderr.trim().slice(0, 400)}

What it means

The computer-use plugin's SSH transport runs the remote agent over `ssh node <agent> <base64 args>` and throws this ExecError when the ssh process exits with a nonzero status. The stderr (trimmed, max 400 chars) is embedded so the developer sees the remote ssh failure cause. It means the remote command never produced a usable run — ssh connectivity, auth, or the remote node invocation failed before a receipt could be parsed.

Solutions

  1. Test connectivity manually: ssh <userHost> 'node --version' to reproduce and see full stderr
  2. Verify the agent is installed by running installRemoteAgent() first (creates .codewhale-cu/agent)
  3. Confirm node is installed and on PATH on the remote host
  4. Check userHost, port, and key config in the computer binding
  5. Inspect the stderr embedded in the error message for the root cause (auth denied, no route, etc.)

Example fix

// before
const ex = sshExec({ transport: "ssh" }); // host wrong / agent missing
await ex.remote({ tool: "platform" });
// after
const ex = sshExec({ transport: "ssh", userHost: "deploy@10.0.0.5", port: 22 });
await installRemoteAgent(ex, computer); // ensure agent exists before remote calls
await ex.remote({ tool: "platform" });
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check ssh reachability and agent presence
const probe = await run("ssh", [...base, "test", "-x", ".codewhale-cu/agent/agent.mjs"]);
if (probe.code !== 0) await installRemoteAgent(ex, computer);

Type guard

function isSshExecError(e) { return e instanceof ExecError && /^ssh .* exited \d+:/i.test(e.message); }

Try / catch

try {
  const reply = await ex.remote(request, opts);
} catch (e) {
  if (e.code === "cancelled") return;
  if (/exited \d+:/.test(e.message)) console.error("ssh failed:", e.message); // stderr embedded
}

Prevention

When it happens

Trigger: Calling executor.remote() when ssh exits nonzero: host unreachable, auth rejected, remote node binary missing, remoteAgent script path missing, or remote node crashes with nonzero exit.

Common situations: SSH key not deployed to the remote machine; wrong userHost/port in the computer binding; remote agent not yet installed at .codewhale-cu/agent; node not on PATH on the remote host; MOTD/shell rc printing to stderr causing ssh -t failures; network/firewall drops mid-run.

Related errors


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

Appendix: source

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

  const portArgs = computer.port ? ["-p", String(computer.port)] : [];
  const remoteAgent = safeRemotePath(computer.agentPath ?? ".codewhale-cu/agent/agent.mjs");
  const base = ["-o", "BatchMode=yes", "-o", "ConnectTimeout=8", "-o", "StrictHostKeyChecking=accept-new", ...portArgs, userHost];
  const ex = {
    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.
 */

View on GitHub (pinned to 73e0f67d83)