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
- Use a numeric port in 1–65535: `ssh://host:2222/path`, or omit the port entirely.
- Check the port variable/expression — log or print the final URL before resolving.
- Use correct bracketed IPv6: `ssh://[2001:db8::1]/path`.
- 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
- Validate ports are integers in 1–65535 before interpolating into URLs.
- Use bracketed IPv6 literals: ssh://[2001:db8::1]/path.
- Print/inspect the final URL when built from variables.
- Prefer configured host aliases over hand-built host:port strings.
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
- ssh:// requires an absolute path, e.g. ssh://host/etc/hosts
- ssh:// requires a host: ssh://<host>/<absolute-path>
- Provider delete URL must not embed an account credential
- ${destination} returned an invalid upload URL
- Destination option endpoint must be an absolute URL
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/d4c69f103cc53731.
Report an issue: GitHub.