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
- Test connectivity manually: ssh <userHost> 'node --version' to reproduce and see full stderr
- Verify the agent is installed by running installRemoteAgent() first (creates .codewhale-cu/agent)
- Confirm node is installed and on PATH on the remote host
- Check userHost, port, and key config in the computer binding
- 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
- Install the remote agent before issuing remote calls
- Verify ssh key auth works to userHost before registering the computer
- Confirm node is on PATH on the remote host
- Keep timeouts generous enough for slow links
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
- ssh : agent returned no JSON receipt
- ssh : timed out
- building bundle fetch client failed
- bundle fetch failed with HTTP status
- bundle fetch request failed
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)