can1357/oh-my-pi · error

imageUrls exposure "ssh" requires imageUrls.sshTarget

Error message

imageUrls exposure "ssh" requires imageUrls.sshTarget

What it means

The ssh exposure needs a destination for the reverse forward in `user@host[:port]` form (config.sshTarget). This error throws when kind is "ssh" but sshTarget is missing. Without it, there is no host to run `ssh -R` against.

Source

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

				const tunnelName = optionString(config, "tunnelName");
				if (!configFile || !tunnelName) {
					throw new Error(
						'imageUrls exposure "named-cloudflared" requires credentials.tunnelToken or options.configFile and options.tunnelName',
					);
				}
				argv = [binary, "tunnel", "--no-autoupdate", "--config", configFile, "run", tunnelName];
			}
			const baseUrl = normalizeBaseUrl(config.publicBaseUrl);
			const { proc } = await spawnUrlTunnel(
				argv,
				() => baseUrl,
				/Registered tunnel connection|Connection [a-z0-9-]+ registered/i,
			);
			return processExposure("named-cloudflared", baseUrl, proc);
		}
		case "ssh": {
			if (!config.publicBaseUrl) throw new Error('imageUrls exposure "ssh" requires imageUrls.publicBaseUrl');
			if (!config.sshTarget) throw new Error('imageUrls exposure "ssh" requires imageUrls.sshTarget');
			const binary = requireBinary("ssh");
			const remotePort = config.sshRemotePort ?? 8787;
			const proc = Bun.spawn(
				[
					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),

View on GitHub (pinned to 9690622007)

Solutions

  1. Set imageUrls.sshTarget to "user@host" or "user@host:port".
  2. Confirm passwordless/keyed ssh access: `ssh user@host true` succeeds non-interactively.
  3. Alternatively pick a tunnel kind (cloudflared, ngrok, bore) that needs no ssh target.

Example fix

// before
"imageUrls": { "exposure": { "kind": "ssh" } }
// after
"imageUrls": { "exposure": { "kind": "ssh", "sshTarget": "deploy@myserver.example.com" } }
Defensive patterns

Strategy: validation

Validate before calling

if (config.kind === "ssh") {
  if (typeof config.sshTarget !== "string" || !/^[^@\s]+@[^@\s]+(:\d+)?$/.test(config.sshTarget)) {
    throw new Error('ssh exposure needs sshTarget like "user@host" or "user@host:22"');
  }
}

Type guard

function hasSshTarget(c: { kind: string; sshTarget?: string }): c is typeof c & { sshTarget: string } {
  return c.kind !== "ssh" || (typeof c.sshTarget === "string" && /^[^@\s]+@[^@\s]+(:\d+)?$/.test(c.sshTarget));
}

Prevention

When it happens

Trigger: Selecting exposure kind "ssh" with publicBaseUrl set but no imageUrls.sshTarget; sshTarget set to an empty string.

Common situations: User assumed sshTarget comes from the machine's ssh config; switched exposure kind from a tunnel (ngrok etc.) to ssh without adding sshTarget; forgot the field when writing config by hand.

Understand the failure class

Background: "Must pass :limit option" / "Missing required option" — required option errors explained — this error's family across 41 libraries.

Related errors


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