can1357/oh-my-pi · error

ssh reverse forward to ${config.sshTarget} exited with code

Error message

ssh reverse forward to ${config.sshTarget} exited with code ${early}

What it means

ssh prints nothing on success, so the adapter races proc.exited against a 1.5s grace period (SSH_READY_GRACE_MS): if the ssh process is still alive after the grace window, the forward is assumed established. If ssh instead exits within that window, this error is thrown with its exit code — meaning authentication failed, the host was unreachable, the remote port was refused, or ssh options were rejected.

Source

Thrown at packages/coding-agent/src/blob-broker/exposure.ts:511

				[
					binary,
					"-o",
					"BatchMode=yes",
					"-o",
					"ExitOnForwardFailure=yes",
					"-N",
					"-R",
					`${remotePort}:127.0.0.1:${port}`,
					config.sshTarget,
				],
				{ env: process.env, stdin: "ignore", stdout: "ignore", stderr: "ignore", cwd: os.homedir() },
			);
			const early = await Promise.race([
				proc.exited.then(code => code),
				Bun.sleep(SSH_READY_GRACE_MS).then(() => null),
			]);
			if (early !== null) {
				throw new Error(`ssh reverse forward to ${config.sshTarget} exited with code ${early}`);
			}
			logger.debug("blob-broker: ssh reverse forward established", {
				target: config.sshTarget,
				remotePort,
				localPort: port,
			});
			return processExposure("ssh", normalizeBaseUrl(config.publicBaseUrl), proc);
		}
	}
}

View on GitHub (pinned to 9690622007)

Solutions

  1. Test `ssh -o BatchMode=yes -R 8787:localhost:0 user@host true` manually to see the real ssh error.
  2. Set up key-based auth and an ssh-agent so ssh never prompts.
  3. Check the remote sshd config allows TCP forwarding (AllowTcpForwarding yes) and the remote port is free.
  4. Verify sshTarget hostname/port and that outbound ssh connectivity exists.
  5. Use ssh options like -o StrictHostKeyChecking=accept-new / ServerAliveInterval for headless reliability.

Example fix

// before
proc spawn args without options → ssh prompts for host key and exits
// after
"exposure": { "kind": "ssh", "sshTarget": "user@host", "options": { "sshOptions": ["-o", "StrictHostKeyChecking=accept-new"] } } // plus working key auth
Defensive patterns

Strategy: try-catch

Validate before calling

const probe = Bun.spawnSync(["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", sshTarget, "true"], { stderr: "pipe" });
if (probe.exitCode !== 0) throw new Error(`ssh to ${sshTarget} unavailable: ${probe.stderr.toString()}`);

Try / catch

try {
  const exposure = await startExposure(config);
} catch (err) {
  if (err instanceof Error && err.message.startsWith("ssh reverse forward")) {
    // surface ssh diagnostics: run with -v manually; check keys, agent, AllowTcpForwarding
    logger.warn("ssh forward failed at startup", { target: config.sshTarget });
  } else throw err;
}

Prevention

When it happens

Trigger: startExposure kind "ssh" where the spawned `ssh -R <remotePort>:localhost:<port> <sshTarget>` process terminates within 1.5 seconds of launch.

Common situations: ssh key has a passphrase and no agent, prompting fails non-interactively; known_hosts/host-key prompt blocks and ssh exits; remote sshd has AllowTcpForwarding disabled; target hostname typo or DNS failure; remote port already bound on the server; ssh binary missing or the network blocks port 22.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/0aad2509304951ec. Report an issue: GitHub.