can1357/oh-my-pi · error
ssh://: unsupported or malformed authority in "${url.href}";
Error message
ssh://: unsupported or malformed authority in "${url.href}"; use ssh://[user@]host[:1-65535]/<absolute-path> What it means
Generic backstop for any ssh:// authority whose decoded literal text does not reconstruct to the canonical `[user@]host[:port]` that WHATWG actually parsed — most notably an empty password (`ssh://user:@host`, `ssh://:@host`). Every valid authority, including percent-encoded aliases, reconstructs exactly, so only genuinely malformed userinfo trips this.
Source
Thrown at packages/coding-agent/src/internal-urls/ssh-protocol.ts:212
if (port === undefined && url.rawHost === `${username ? `${decodeOr(username)}@` : ""}${decodeOr(bareHost)}:`) {
throw new Error(`ssh://: empty port in "${url.href}"; use ssh://host:<1-65535>/<path> or drop the colon`);
}
// A literal but empty userinfo (`ssh://@host`) sets username to "" — WHATWG drops
// the `@` from hostname, but rawHost keeps the leading `@`. A percent-encoded
// alias like `%40prod` decodes to `@prod` in rawHost too, but its hostname keeps
// `%40`, so the reconstruction is `@@prod` and is left alone.
if (username === undefined && url.rawHost === `@${decodeOr(bareHost)}${port !== undefined ? `:${port}` : ""}`) {
throw new Error(`ssh://: empty username in "${url.href}"; drop the leading '@' or provide a username before it`);
}
// Backstop for any remaining stray/empty authority marker the explicit checks
// above do not name — notably an empty password (`ssh://user:@host`, `ssh://:@host`,
// where `url.password === ""`). `rawHost` keeps the literal marker, so it differs
// from the canonical decoded `[user@]host[:port]` WHATWG actually parsed. Every
// valid authority — including percent-encoded reserved-char aliases — reconstructs
// to exactly `rawHost`, so only malformed userinfo trips this.
const canonicalAuthority = `${url.username ? `${decodeOr(url.username)}@` : ""}${decodeOr(bareHost)}${port !== undefined ? `:${port}` : ""}`;
if (url.rawHost !== canonicalAuthority) {
throw new Error(
`ssh://: unsupported or malformed authority in "${url.href}"; use ssh://[user@]host[:1-65535]/<absolute-path>`,
);
}
const items = await loadConfiguredHosts(cwd);
// A literal user/port in the URL is an authority override. A configured alias
// is addressed only by its (percent-encoded) name, never with a separate
// user/port — so reject an override on a configured bare name, else opaque.
if (username || port !== undefined) {
const decodedBareHost = decodeOr(bareHost);
if (items.some(entry => entry.name === bareHost || entry.name === decodedBareHost)) {
throw new Error(
`ssh://: user/port overrides are not allowed for the configured host "${decodedBareHost}"; use ssh://${bareHost}/<path> or an unconfigured hostname`,
);
}
const sshUser = username ? decodeOr(username) : undefined;
const sshTargetHost = decodeOr(sshHost);
const name = `${sshUser ? `${sshUser}@` : ""}${sshTargetHost}${port !== undefined ? `:${port}` : ""}`;View on GitHub (pinned to 9690622007)
Solutions
- Fix the authority to the shape ssh://[user@]host[:port]/path
- Remove the leftover `:`/`@` markers: `ssh://user@host/path`
- Percent-encode any reserved characters actually intended in user/host (e.g. `%40` for `@`)
Example fix
// before
resolve('ssh://user:@prod.example.com/etc/hosts')
// after
resolve('ssh://user@prod.example.com/etc/hosts') Defensive patterns
Strategy: validation
Validate before calling
const u = new URL(candidate);
const decode = (s: string) => { try { return decodeURIComponent(s); } catch { return s; } };
const host = u.hostname, port = u.port ? Number(u.port) : undefined;
const canonical = `${u.username ? `${decode(u.username)}@` : ''}${decode(host)}${port !== undefined ? `:${port}` : ''}`;
if (u.protocol === 'ssh:' && (u.rawHost || host) !== canonical) throw new Error(`malformed authority in ${candidate}`); Type guard
function hasCanonicalSshAuthority(u: URL): boolean { return (u.rawHost ?? u.hostname) === canonicalSshAuthority(u); } Try / catch
try {
const res = await handler.resolve(url, ctx);
} catch (e) {
if (e instanceof Error && e.message.includes('unsupported or malformed authority')) {
// rebuild the authority as [user@]host[:port], then retry
} else throw e;
} Prevention
- Build ssh URLs from structured parts (user, host, port) instead of string editing
- Percent-encode reserved characters in usernames/hosts deliberately (e.g. %40 for @)
- Validate the URL with new URL/URL.canParse before handing it to the handler
When it happens
Trigger: Resolving URLs like `ssh://user:@host/path` or `ssh://:@host/path` (empty `url.password`), or any other stray/empty authority marker where `url.rawHost !== canonicalAuthority` after the explicit empty-port/empty-username checks.
Common situations: Deleting a password but leaving the colon; templates emitting `ssh://${user}:${pass}@${host}` with an empty pass; hand-edited URLs with leftover separators.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- ssh://: empty port in "${url.href}"; use ssh://host:<1-65535
- ssh://: empty username in "${url.href}"; drop the leading '@
- ssh:// requires a host before the path: ssh://<host>${rawPat
- imageUrls exposure "ssh" requires imageUrls.publicBaseUrl
- imageUrls exposure "ssh" requires imageUrls.sshTarget
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/fbb61a04264e9b30.
Report an issue: GitHub.