can1357/oh-my-pi · error

ssh://: invalid host or port in "${url.href}"; use ssh://hos

Error message

ssh://: invalid host or port in "${url.href}"; use ssh://host[:1-65535]/<absolute-path>

What it means

When WHATWG `new URL` cannot parse the URL, the internal-URL parser falls back to a lenient regex parse. For ssh:// that only happens with 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 on the default port. `resolveTarget` guards with `URL.canParse(url.href)` and rejects such URLs up front.

Source

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

}

/**
 * Resolve the URL authority to an SSH connection target. With no explicit
 * user/port, the full DECODED authority (`url.rawHost`) is matched against a
 * configured host name, so percent-encoded reserved-char aliases (e.g.
 * `alice%40prod` → `alice@prod`) resolve correctly. A literal `user@`/`:port`
 * in the URL is an override: it is rejected on a configured bare name (the
 * ControlMaster/host-info caches key on `name` alone) and otherwise treated as
 * 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 {

View on GitHub (pinned to 9690622007)

Solutions

  1. Use a numeric port in 1–65535: `ssh://host:2222/path`, or omit the port entirely.
  2. Check the port variable/expression — log or print the final URL before resolving.
  3. Use correct bracketed IPv6: `ssh://[2001:db8::1]/path`.
  4. If the host name contains a colon as part of an alias (e.g. `alice@prod`), percent-encode reserved chars: `ssh://alice%40prod/path`.

Example fix

// before
await readResource("ssh://prod:abc/etc/hosts");
// after
await readResource("ssh://prod:22/etc/hosts");
Defensive patterns

Strategy: validation

Validate before calling

function assertParsableSshUrl(url: string): void {
  if (url.startsWith("ssh://") && !URL.canParse(url)) {
    throw new Error(`Malformed ssh:// authority (check host:port): ${url}`);
  }
}

Try / catch

try {
  return await sshHandler.resolve(url);
} catch (err) {
  if (err instanceof Error && err.message.includes("invalid host or port")) {
    // sanitize: strip bad port, retry with default
    const fixed = url.replace(/:(\D|6[5-9]\d{3})/, "");
    return sshHandler.resolve(parseInternalUrl(fixed));
  }
  throw err;
}

Prevention

When it happens

Trigger: `SshProtocolHandler.resolve()`/`.write()` (via `resolveTarget`) with a `ssh://` URL whose authority WHATWG URL parsing rejects: non-numeric port (`ssh://prod:abc/path`), port out of range (`ssh://host:65536/path`), or a malformed IPv6 literal such as an unclosed bracket.

Common situations: Typos in ports; pasting a `host:port` pair where the port column got corrupted; hand-built URLs like `ssh://host:${portVar}/path` where the variable is empty or non-numeric; IPv6 addresses written without full bracket syntax.

Related errors


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