{"record":{"id":"ef472b4ec4f7905a","repo":"can1357/oh-my-pi","slug":"ssh-requires-a-host-ssh-host-absolute-pat","errorCode":null,"errorMessage":"ssh:// requires a host: ssh://<host>/<absolute-path>","messagePattern":"ssh:// requires a host: ssh://<host>/<absolute-path>","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/ssh-protocol.ts","lineNumber":153,"sourceCode":" * an opaque OpenSSH destination so plain `~/.ssh/config` aliases work.\n */\nasync function resolveTarget(url: InternalUrl, cwd?: string): Promise<SSHConnectionTarget> {\n\t// `parseInternalUrl` falls back to a lenient regex parse when WHATWG `new URL`\n\t// rejects the input. For ssh:// that only happens on a malformed authority — an\n\t// invalid or out-of-range port (`prod:abc`, `host:65536`) or a bad IPv6 literal —\n\t// which would otherwise be mis-read as an opaque host and silently connect to the\n\t// default port. Reject it before resolving.\n\tif (!URL.canParse(url.href)) {\n\t\tthrow new Error(`ssh://: invalid host or port in \"${url.href}\"; use ssh://host[:1-65535]/<absolute-path>`);\n\t}\n\t// WHATWG `hostname` is bracketed only for a *valid* IPv6 literal, so a bracketed\n\t// host is unambiguously IPv6 — hand OpenSSH the bare address. Percent-encoded\n\t// bracketed aliases (e.g. `%5Bprod%3A2222%5D`) keep their literal brackets in the\n\t// decoded `rawHost`, so they are matched and forwarded verbatim, never stripped.\n\tconst bareHost = url.hostname;\n\tconst rawAuthority = url.rawHost || bareHost;\n\tif (!bareHost && !rawAuthority) {\n\t\tthrow new Error(\"ssh:// requires a host: ssh://<host>/<absolute-path>\");\n\t}\n\t// `decodeOr` fails open, so a malformed percent-escape (`%ZZ`) in the authority\n\t// would otherwise pass the canonical check below and reach OpenSSH literally.\n\t// Reject it up front — the path decoder fails closed for the same bad escapes.\n\tfor (const part of [url.username, bareHost]) {\n\t\tif (part.includes(\"%\")) {\n\t\t\ttry {\n\t\t\t\tdecodeURIComponent(part);\n\t\t\t} catch {\n\t\t\t\tthrow new Error(`ssh://: invalid percent-escape in authority \"${url.href}\"`);\n\t\t\t}\n\t\t}\n\t}\n\tif (url.password) {\n\t\tthrow new Error(\n\t\t\t\"ssh://: password authentication is not supported; ssh:// uses key/agent auth — drop the ':<password>' from the URL\",\n\t\t);\n\t}","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/ssh-protocol.ts#L135-L171","documentation":"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.)","triggerScenarios":"`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.","commonSituations":"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.","solutions":["Supply a host: `ssh://myhost/etc/hosts` (any destination OpenSSH can resolve, including `~/.ssh/config` aliases).","Check the variable/config supplying the host — it is empty; set it before building the URL.","Read bare `ssh://` (no path) to list configured hosts and pick the right one."],"exampleFix":"// before\nawait readResource(`ssh:///etc/hosts`); // missing host\n// after\nawait readResource(`ssh://prod/etc/hosts`);","handlingStrategy":"validation","validationCode":"function assertSshHostPresent(url: string): void {\n  const u = new URL(url);\n  if (u.protocol === \"ssh:\" && !u.hostname) {\n    throw new Error(`ssh:// URL requires a host: ${url}`);\n  }\n}","typeGuard":"function hasSshHost(url: URL): boolean {\n  return url.protocol === \"ssh:\" && url.hostname.length > 0;\n}","tryCatchPattern":"try {\n  return await sshHandler.resolve(url);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith(\"ssh:// requires a host:\")) {\n    // host variable was empty — list configured hosts for the user\n    return sshHandler.resolve(parseInternalUrl(\"ssh://\"));\n  }\n  throw err;\n}","preventionTips":["Check host config/variables are non-empty before building ssh:// URLs.","Never hand-write ssh:///path forms; always prefix the host.","Use bare ssh:// to discover available configured hosts."],"tags":["url","ssh-protocol","validation","missing-host"],"backgroundTag":"invalid-url-syntax","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}