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
- Set imageUrls.sshTarget to "user@host" or "user@host:port".
- Confirm passwordless/keyed ssh access: `ssh user@host true` succeeds non-interactively.
- 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
- Provide sshTarget in user@host[:port] form
- Test non-interactive ssh access (`ssh -o BatchMode=yes user@host true`) before configuring
- Use an ssh config alias consistently between sshTarget and ~/.ssh/config
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
- imageUrls exposure "ssh" requires imageUrls.publicBaseUrl
- imageUrls exposure "named-cloudflared" requires imageUrls.pu
- No known OAuth callback port for '${provider}'. Use device-c
- ssh://: user/port overrides are not allowed for the configur
- SSH key not found: ${keyPath}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/2cae7f80031cb999.
Report an issue: GitHub.