can1357/oh-my-pi · error

ssh:// requires a host before the path: ssh://<host>${rawPat

Error message

ssh:// requires a host before the path: ssh://<host>${rawPath} (host-less ssh://${rawPath} is not valid)

What it means

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.

Source

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

/** Format a one-level remote directory listing — mirrors buildDirectoryResource's plain `name/` lines. */
function formatDirListing(entries: readonly RemoteDirEntry[]): string {
	if (entries.length === 0) return "(empty directory)";
	return entries.map(entry => `${entry.name}${entry.isDirectory ? "/" : ""}`).join("\n");
}

export class SshProtocolHandler implements ProtocolHandler {
	readonly scheme = "ssh";
	readonly immutable = false;

	async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
		// Bare `ssh://` (or `ssh:///`) with no host lists the configured hosts. A
		// host-less URL that still carries a path (`ssh:///etc/hosts`) is malformed —
		// reject it instead of silently dropping the path and listing hosts.
		if (!(url.rawHost || url.hostname)) {
			const rawPath = url.rawPathname ?? url.pathname;
			if (rawPath && rawPath !== "/") {
				throw new Error(
					`ssh:// requires a host before the path: ssh://<host>${rawPath} (host-less ssh://${rawPath} is not valid)`,
				);
			}
			return this.#resolveHostIndex(url, context?.cwd);
		}
		const target = await resolveTarget(url, context?.cwd);
		const remotePath = remotePathFromUrl(url);
		// Classify before reading. A FIFO with no writer would block `head` until the
		// timeout, and a device (e.g. /dev/zero) would stream the whole probe, so a
		// special file must fail fast. Only a regular file is read; a directory lists.
		// `missing`/stat-failure falls through to the read so its original remote stderr
		// (e.g. "No such file or directory") still surfaces.
		let kind: RemotePathKind | undefined;
		try {
			kind = await statRemotePath(target, remotePath, { signal: context?.signal });
		} catch {
			// stat failed (host/connection issue) — fall through; the read gives a clearer error.
		}

View on GitHub (pinned to 9690622007)

Solutions

  1. Insert the host before the path: `ssh://prod/etc/hosts`
  2. If you meant the host index, read bare `ssh://` without a path
  3. Fix the template so the host variable is never empty

Example fix

// before
resolve('ssh:///etc/hosts')
// after
resolve('ssh://prod.example.com/etc/hosts')
Defensive patterns

Strategy: validation

Validate before calling

const u = new URL(candidate);
if (u.protocol === 'ssh:' && !u.hostname && u.pathname && u.pathname !== '/') throw new Error(`missing host before path: use ssh://<host>${u.pathname}`);

Type guard

function hasSshHost(u: URL): boolean { return Boolean(u.hostname || (u.rawHost ?? '')); }

Try / catch

try {
  const res = await handler.resolve(url, ctx);
} catch (e) {
  if (e instanceof Error && e.message.includes('requires a host before the path')) {
    // insert the host segment, then retry
  } else throw e;
}

Prevention

When it happens

Trigger: 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).

Common situations: Dropping the host when editing a URL; a template `ssh://${host}${path}` with an empty host variable; typo leaving only two slashes.

Related errors


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