{"record":{"id":"5a58e3330914794c","repo":"can1357/oh-my-pi","slug":"ssh-requires-a-host-before-the-path-ssh-hos","errorCode":null,"errorMessage":"ssh:// requires a host before the path: ssh://<host>${rawPath} (host-less ssh://${rawPath} is not valid)","messagePattern":"ssh:// requires a host before the path: ssh://<host>(.+?) \\(host-less ssh://(.+?) is not valid\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/ssh-protocol.ts","lineNumber":268,"sourceCode":"\n/** Format a one-level remote directory listing — mirrors buildDirectoryResource's plain `name/` lines. */\nfunction formatDirListing(entries: readonly RemoteDirEntry[]): string {\n\tif (entries.length === 0) return \"(empty directory)\";\n\treturn entries.map(entry => `${entry.name}${entry.isDirectory ? \"/\" : \"\"}`).join(\"\\n\");\n}\n\nexport class SshProtocolHandler implements ProtocolHandler {\n\treadonly scheme = \"ssh\";\n\treadonly immutable = false;\n\n\tasync resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {\n\t\t// Bare `ssh://` (or `ssh:///`) with no host lists the configured hosts. A\n\t\t// host-less URL that still carries a path (`ssh:///etc/hosts`) is malformed —\n\t\t// reject it instead of silently dropping the path and listing hosts.\n\t\tif (!(url.rawHost || url.hostname)) {\n\t\t\tconst rawPath = url.rawPathname ?? url.pathname;\n\t\t\tif (rawPath && rawPath !== \"/\") {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`ssh:// requires a host before the path: ssh://<host>${rawPath} (host-less ssh://${rawPath} is not valid)`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn this.#resolveHostIndex(url, context?.cwd);\n\t\t}\n\t\tconst target = await resolveTarget(url, context?.cwd);\n\t\tconst remotePath = remotePathFromUrl(url);\n\t\t// Classify before reading. A FIFO with no writer would block `head` until the\n\t\t// timeout, and a device (e.g. /dev/zero) would stream the whole probe, so a\n\t\t// special file must fail fast. Only a regular file is read; a directory lists.\n\t\t// `missing`/stat-failure falls through to the read so its original remote stderr\n\t\t// (e.g. \"No such file or directory\") still surfaces.\n\t\tlet kind: RemotePathKind | undefined;\n\t\ttry {\n\t\t\tkind = await statRemotePath(target, remotePath, { signal: context?.signal });\n\t\t} catch {\n\t\t\t// stat failed (host/connection issue) — fall through; the read gives a clearer error.\n\t\t}","sourceCodeStart":250,"sourceCodeEnd":286,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/ssh-protocol.ts#L250-L286","documentation":"Bare `ssh://` (no host) resolves to an index of configured hosts, but `ssh:///etc/hosts` — no host AND a path — is ambiguous: honoring the path would require a host to connect to. The handler rejects it rather than silently dropping the path and returning the host list.","triggerScenarios":"Resolving an ssh:// URL with both `rawHost` and `hostname` empty while the raw pathname is present and not `/`, e.g. `ssh:///etc/hosts` (missing host segment after the double slash).","commonSituations":"Dropping the host when editing a URL; a template `ssh://${host}${path}` with an empty host variable; typo leaving only two slashes.","solutions":["Insert the host before the path: `ssh://prod/etc/hosts`","If you meant the host index, read bare `ssh://` without a path","Fix the template so the host variable is never empty"],"exampleFix":"// before\nresolve('ssh:///etc/hosts')\n// after\nresolve('ssh://prod.example.com/etc/hosts')","handlingStrategy":"validation","validationCode":"const u = new URL(candidate);\nif (u.protocol === 'ssh:' && !u.hostname && u.pathname && u.pathname !== '/') throw new Error(`missing host before path: use ssh://<host>${u.pathname}`);","typeGuard":"function hasSshHost(u: URL): boolean { return Boolean(u.hostname || (u.rawHost ?? '')); }","tryCatchPattern":"try {\n  const res = await handler.resolve(url, ctx);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('requires a host before the path')) {\n    // insert the host segment, then retry\n  } else throw e;\n}","preventionTips":["Never interpolate an empty host variable into ssh://${host}${path} templates","Read bare `ssh://` only when you want the configured-host index","Keep host and path in separate, individually validated variables"],"tags":["ssh","url-parsing","missing-host"],"backgroundTag":"missing-url-host","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}