can1357/oh-my-pi · error

ssh://: invalid percent-escape in authority "${url.href}"

Error message

ssh://: invalid percent-escape in authority "${url.href}"

What it means

The lenient URL parser fails open on malformed percent-escapes in the authority, so a bad escape in the username or host (e.g. `%ZZ`) would pass the canonical-authority check and reach OpenSSH literally. To keep path and authority decoding consistent (the path decoder fails closed), `resolveTarget` explicitly decodes `url.username` and the bare host and rejects any un-decodable percent-escape.

Source

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

	}
	// WHATWG `hostname` is bracketed only for a *valid* IPv6 literal, so a bracketed
	// host is unambiguously IPv6 — hand OpenSSH the bare address. Percent-encoded
	// bracketed aliases (e.g. `%5Bprod%3A2222%5D`) keep their literal brackets in the
	// decoded `rawHost`, so they are matched and forwarded verbatim, never stripped.
	const bareHost = url.hostname;
	const rawAuthority = url.rawHost || bareHost;
	if (!bareHost && !rawAuthority) {
		throw new Error("ssh:// requires a host: ssh://<host>/<absolute-path>");
	}
	// `decodeOr` fails open, so a malformed percent-escape (`%ZZ`) in the authority
	// would otherwise pass the canonical check below and reach OpenSSH literally.
	// Reject it up front — the path decoder fails closed for the same bad escapes.
	for (const part of [url.username, bareHost]) {
		if (part.includes("%")) {
			try {
				decodeURIComponent(part);
			} catch {
				throw new Error(`ssh://: invalid percent-escape in authority "${url.href}"`);
			}
		}
	}
	if (url.password) {
		throw new Error(
			"ssh://: password authentication is not supported; ssh:// uses key/agent auth — drop the ':<password>' from the URL",
		);
	}
	const isIpv6Literal = bareHost.startsWith("[") && bareHost.endsWith("]");
	const sshHost = isIpv6Literal ? bareHost.slice(1, -1) : bareHost;
	const username = url.username || undefined;
	const port = url.port ? Number(url.port) : undefined;
	if (port === 0) {
		throw new Error("ssh://: port 0 is not a valid SSH port; use ssh://host:<1-65535>/<path> or omit the port");
	}
	// An empty port (`ssh://prod:/path`, `ssh://user@host:/path`, including
	// percent-encoded authority parts) parses cleanly with `url.port === ""`, so it
	// slips past the malformed-authority guard and would be read as "no port" —

View on GitHub (pinned to 9690622007)

Solutions

  1. Percent-encode a literal `%` in the username/host as `%25` (e.g. `ssh://user%25x@host/path`).
  2. Encode reserved characters in the username with `encodeURIComponent` (e.g. `alice@prod` → `alice%40prod`).
  3. Inspect the URL string for stray `%` characters and remove or correctly encode them.

Example fix

// before
await readResource(`ssh://${user}@prod/etc/hosts`); // user = "100%admin"
// after
await readResource(`ssh://${encodeURIComponent(user)}@prod/etc/hosts`);
Defensive patterns

Strategy: validation

Validate before calling

function buildSshAuthority(user?: string, host: string): string {
  const enc = (s: string) => encodeURIComponent(s);
  return user ? `${enc(user)}@${host}` : host; // encode username; host must be a plain name/IP
}

Type guard

function decodableAuthorityPart(part: string): boolean {
  if (!part.includes("%")) return true;
  try { decodeURIComponent(part); return true; } catch { return false; }
}

Try / catch

try {
  return await sshHandler.resolve(url);
} catch (err) {
  if (err instanceof Error && err.message.includes("invalid percent-escape in authority")) {
    // rebuild with encodeURIComponent on username; encode literal '%' as %25
    return sshHandler.resolve(parseInternalUrl(`ssh://${encodeURIComponent(user)}@${host}/path`));
  }
  throw err;
}

Prevention

When it happens

Trigger: `SshProtocolHandler.resolve()`/`.write()` (via `resolveTarget`) with a `ssh://` URL whose username or hostname contains a `%` that is not a valid percent-escape, e.g. `ssh://100%user@host/path` or `ssh://ho%st/path`.

Common situations: Usernames containing a literal `%` (some LDAP/AD conventions) interpolated without encoding; corrupted or truncated URLs leaving a stray `%`; manual percent-encoding mistakes (`%Z`, single hex digit, `%` at end).

Related errors


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