can1357/oh-my-pi · error

ssh://: empty username in "${url.href}"; drop the leading '@

Error message

ssh://: empty username in "${url.href}"; drop the leading '@' or provide a username before it

What it means

A URL like `ssh://@host/path` has empty userinfo — WHATWG drops the `@` from hostname but `rawHost` keeps the leading `@`. Since a user was clearly intended but none provided, the handler rejects it instead of silently connecting without a username.

Source

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

	// host (+ user) catches the empty port, while a percent-encoded alias like
	// `prod%3A` (whose decoded host already ends in `:`) reconstructs to `prod::`
	// and is left alone.
	const decodeOr = (s: string): string => {
		try {
			return decodeURIComponent(s);
		} catch {
			return s;
		}
	};
	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.

View on GitHub (pinned to 9690622007)

Solutions

  1. Remove the leading `@` to use the default login user: `ssh://prod/path`
  2. Provide a username before the `@`: `ssh://deploy@prod/path`
  3. Fix the template producing `${user}@` when the user variable is empty

Example fix

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

Strategy: validation

Validate before calling

const u = new URL(candidate);
if (u.protocol === 'ssh:' && !u.username && (u.rawHost ?? '').startsWith('@')) throw new Error(`empty userinfo in ${candidate}; drop '@' or add a username`);

Type guard

function hasNonEmptySshUserinfo(u: URL): boolean { return !(u.rawHost ?? '').startsWith('@'); }

Try / catch

try {
  const res = await handler.resolve(url, ctx);
} catch (e) {
  if (e instanceof Error && e.message.includes('empty username in')) {
    // drop the '@' or supply a username, then retry
  } else throw e;
}

Prevention

When it happens

Trigger: Resolving an ssh:// URL whose `rawHost` equals `@<decoded host>[:port]` while `url.username` is undefined — i.e. `ssh://@prod/etc/hosts` or `ssh://@prod:2222/etc/hosts`. A percent-encoded `%40prod` alias does NOT trip this (it reconstructs to `@@prod` and is left alone).

Common situations: Deleting a username but leaving the `@`; a template `ssh://${user}@${host}/path` where the user variable was empty; hand-edited URLs.

Related errors


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