can1357/oh-my-pi · error

ssh:// requires a host: ssh://<host>/<absolute-path>

Error message

ssh:// requires a host: ssh://<host>/<absolute-path>

What it means

The ssh:// scheme requires a host in the authority. If both WHATWG `hostname` and the raw decoded host are empty (e.g. `ssh:///etc/hosts` — a path with no host), `resolveTarget` throws this error, because there is no destination for OpenSSH to connect to. (A completely bare `ssh://` without a path is handled separately as the configured-hosts index.)

Source

Thrown at packages/coding-agent/src/internal-urls/ssh-protocol.ts:153

 * an opaque OpenSSH destination so plain `~/.ssh/config` aliases work.
 */
async function resolveTarget(url: InternalUrl, cwd?: string): Promise<SSHConnectionTarget> {
	// `parseInternalUrl` falls back to a lenient regex parse when WHATWG `new URL`
	// rejects the input. For ssh:// that only happens on a malformed authority — an
	// invalid or out-of-range port (`prod:abc`, `host:65536`) or a bad IPv6 literal —
	// which would otherwise be mis-read as an opaque host and silently connect to the
	// default port. Reject it before resolving.
	if (!URL.canParse(url.href)) {
		throw new Error(`ssh://: invalid host or port in "${url.href}"; use ssh://host[:1-65535]/<absolute-path>`);
	}
	// WHATWG `hostname` is bracketed only for a *valid* IPv6 literal, so a bracketed
	// host is unambiguously IPv6 — hand OpenSSH the bare address. Percent-encoded
	// bracketed aliases (e.g. `%5Bprod%3A2222%5D`) keep their literal brackets in the
	// decoded `rawHost`, so they are matched and forwarded verbatim, never stripped.
	const bareHost = url.hostname;
	const rawAuthority = url.rawHost || bareHost;
	if (!bareHost && !rawAuthority) {
		throw new Error("ssh:// requires a host: ssh://<host>/<absolute-path>");
	}
	// `decodeOr` fails open, so a malformed percent-escape (`%ZZ`) in the authority
	// would otherwise pass the canonical check below and reach OpenSSH literally.
	// Reject it up front — the path decoder fails closed for the same bad escapes.
	for (const part of [url.username, bareHost]) {
		if (part.includes("%")) {
			try {
				decodeURIComponent(part);
			} catch {
				throw new Error(`ssh://: invalid percent-escape in authority "${url.href}"`);
			}
		}
	}
	if (url.password) {
		throw new Error(
			"ssh://: password authentication is not supported; ssh:// uses key/agent auth — drop the ':<password>' from the URL",
		);
	}

View on GitHub (pinned to 9690622007)

Solutions

  1. Supply a host: `ssh://myhost/etc/hosts` (any destination OpenSSH can resolve, including `~/.ssh/config` aliases).
  2. Check the variable/config supplying the host — it is empty; set it before building the URL.
  3. Read bare `ssh://` (no path) to list configured hosts and pick the right one.

Example fix

// before
await readResource(`ssh:///etc/hosts`); // missing host
// after
await readResource(`ssh://prod/etc/hosts`);
Defensive patterns

Strategy: validation

Validate before calling

function assertSshHostPresent(url: string): void {
  const u = new URL(url);
  if (u.protocol === "ssh:" && !u.hostname) {
    throw new Error(`ssh:// URL requires a host: ${url}`);
  }
}

Type guard

function hasSshHost(url: URL): boolean {
  return url.protocol === "ssh:" && url.hostname.length > 0;
}

Try / catch

try {
  return await sshHandler.resolve(url);
} catch (err) {
  if (err instanceof Error && err.message.startsWith("ssh:// requires a host:")) {
    // host variable was empty — list configured hosts for the user
    return sshHandler.resolve(parseInternalUrl("ssh://"));
  }
  throw err;
}

Prevention

When it happens

Trigger: `SshProtocolHandler.resolve()`/`.write()` (via `resolveTarget`) with a `ssh://` URL that has an empty authority but a non-empty path, e.g. `ssh:///etc/hosts` or `ssh://` built from an unset host variable.

Common situations: Template expansion where the host variable is empty or undefined (`ssh://${hostEnv}/path` with `hostEnv=""`); copy-pasting a URL and deleting the host by accident; config files where the host field is blank.

Related errors


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