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
- Percent-encode a literal `%` in the username/host as `%25` (e.g. `ssh://user%25x@host/path`).
- Encode reserved characters in the username with `encodeURIComponent` (e.g. `alice@prod` → `alice%40prod`).
- 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
- encodeURIComponent() usernames containing '@', '%', or ':' before embedding in the URL.
- Encode a literal '%' as %25 in authority parts.
- Avoid pasting credentials/usernames with special characters unencoded.
- Keep hosts as plain names, IPs, or properly bracketed IPv6 literals.
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
- Invalid URL encoding in ssh:// path: ${url.href}
- ssh:// does not support URL query strings; percent-encode a
- ssh:// does not support URL fragments; percent-encode a lite
- Invalid URL encoding in vault:// path: ${url.href}
- Invalid URL encoding in memory:// path: ${url.href}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/f7347cfa6dbea46d.
Report an issue: GitHub.