Hmbown/CodeWhale · error · ExecError
scp failed
Error message
scp ${rel} failed: ${r.stderr.trim().slice(0, 300)} What it means
During installRemoteAgent(), each agent file is uploaded with scp; when any scp invocation exits nonzero this ExecError is thrown with the relative file name and trimmed stderr (max 300 chars). It identifies exactly which agent file failed to copy and why.
Solutions
- Check the local file exists: the error's rel names it; rebuild the plugin srcDir if missing
- Verify the remote directory still exists (.codewhale-cu/agent) after mkdir
- Ensure scp is installed locally and the port flag (-P) matches the computer's port
- Retry installRemoteAgent() — transient network drops are common
- Read embedded stderr for cause (permission denied vs no such file)
Example fix
// before
await installRemoteAgent(ex, computer); // scp backends/screen.mjs failed: no such file
// after
for (const rel of rels) {
const p = rel === "agent.mjs" ? path.join(PLUGIN_ROOT, "agent.mjs") : path.join(srcDir, rel.slice(4));
if (!fs.existsSync(p)) throw new Error(`missing local agent file: ${p}`); // detect before scp
}
await installRemoteAgent(ex, computer); Defensive patterns
Strategy: retry
Validate before calling
// ensure every local file to upload exists before starting scp loop
for (const rel of rels) {
const p = rel === "agent.mjs" ? path.join(PLUGIN_ROOT, "agent.mjs") : path.join(srcDir, rel.slice(4));
if (!fs.existsSync(p)) throw new Error(`missing local file for ${rel}: ${p}`);
} Type guard
function isScpFailure(e) { return e instanceof ExecError && e.message.startsWith("scp "); } Try / catch
try {
await installRemoteAgent(ex, computer);
} catch (e) {
if (e.message.startsWith("scp ")) {
// transient network drop is common — retry with backoff
return retryWithBackoff(() => installRemoteAgent(ex, computer), 3);
}
throw e;
} Prevention
- Rebuild the plugin so srcDir contains all backend files before install
- Pass the correct port (-P) when the computer uses a nonstandard ssh port
- Verify scp exists on the local machine
- Keep the remote .codewhale-cu tree intact between mkdir and scp
When it happens
Trigger: installRemoteAgent() loop hits an scp failure: local file missing (srcDir not built), remote dir removed between mkdir and scp, permission denied, scp binary absent, or connection drops mid-transfer.
Common situations: Plugin build output (srcDir) stale or deleted so localPath doesn't exist; remote .codewhale-cu wiped by another process; scp not installed on the local machine; custom ssh port mismatched (port not passed); network interruption on large transfers.
Related errors
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/19e59966e466c87f.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/transport.mjs:288
/** Push the self-contained remote agent + src tree to an ssh computer. */
export async function installRemoteAgent(computer) {
const ex = sshExec(computer);
const srcDir = path.join(PLUGIN_ROOT, "src");
const rels = ["agent.mjs"];
for (const dir of ["", "backends"]) {
const full = path.join(srcDir, dir);
for (const f of fs.readdirSync(full)) {
if (f.endsWith(".mjs") || f.endsWith(".m") || f.endsWith(".h")) rels.push(`src/${dir ? dir + "/" : ""}${f}`);
}
}
const marker = ".codewhale-cu/agent";
let r = await run("ssh", [...ex.base, "mkdir", "-p", `${marker}/src/backends`], { timeoutMs: 15_000 });
if (r.code !== 0) throw new ExecError(`ssh ${ex.userHost}: mkdir failed: ${r.stderr.trim().slice(0, 300)}`, r);
for (const rel of rels) {
const localPath = rel === "agent.mjs" ? path.join(PLUGIN_ROOT, "agent.mjs") : path.join(srcDir, rel.slice(4));
const dest = safeRemotePath(`${marker}/${rel}`);
r = await run("scp", [...(computer.port ? ["-P", String(computer.port)] : []), localPath, `${ex.userHost}:${dest}`], { timeoutMs: 30_000 });
if (r.code !== 0) throw new ExecError(`scp ${rel} failed: ${r.stderr.trim().slice(0, 300)}`, r);
}
// Probe remote platform via the agent itself.
const reply = await ex.remote({ tool: "platform" });
return { installed: rels.length, remotePlatform: reply.platform, agentPath: `${marker}/agent.mjs` };
}
/** hdc (HarmonyOS) executor. Commands run on-device; files pull to local tmp. */
export function hdcExec(computer) {
const targetArgs = computer.target ? ["-t", computer.target] : [];
const shell = (args, opts = {}) => run("hdc", [...targetArgs, "shell", ...args], opts);
return {
kind: "hdc",
targetArgs,
run,
runOk,
shell,
async pullFile(remotePath, localPath, opts = {}) {
// HDC device captures use absolute paths; SSH agent paths are relative.View on GitHub (pinned to 73e0f67d83)