{"record":{"id":"f7347cfa6dbea46d","repo":"can1357/oh-my-pi","slug":"ssh-invalid-percent-escape-in-authority-url","errorCode":null,"errorMessage":"ssh://: invalid percent-escape in authority \"${url.href}\"","messagePattern":"ssh://: invalid percent-escape in authority \"(.+?)\"","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/ssh-protocol.ts","lineNumber":163,"sourceCode":"\t}\n\t// WHATWG `hostname` is bracketed only for a *valid* IPv6 literal, so a bracketed\n\t// host is unambiguously IPv6 — hand OpenSSH the bare address. Percent-encoded\n\t// bracketed aliases (e.g. `%5Bprod%3A2222%5D`) keep their literal brackets in the\n\t// decoded `rawHost`, so they are matched and forwarded verbatim, never stripped.\n\tconst bareHost = url.hostname;\n\tconst rawAuthority = url.rawHost || bareHost;\n\tif (!bareHost && !rawAuthority) {\n\t\tthrow new Error(\"ssh:// requires a host: ssh://<host>/<absolute-path>\");\n\t}\n\t// `decodeOr` fails open, so a malformed percent-escape (`%ZZ`) in the authority\n\t// would otherwise pass the canonical check below and reach OpenSSH literally.\n\t// Reject it up front — the path decoder fails closed for the same bad escapes.\n\tfor (const part of [url.username, bareHost]) {\n\t\tif (part.includes(\"%\")) {\n\t\t\ttry {\n\t\t\t\tdecodeURIComponent(part);\n\t\t\t} catch {\n\t\t\t\tthrow new Error(`ssh://: invalid percent-escape in authority \"${url.href}\"`);\n\t\t\t}\n\t\t}\n\t}\n\tif (url.password) {\n\t\tthrow new Error(\n\t\t\t\"ssh://: password authentication is not supported; ssh:// uses key/agent auth — drop the ':<password>' from the URL\",\n\t\t);\n\t}\n\tconst isIpv6Literal = bareHost.startsWith(\"[\") && bareHost.endsWith(\"]\");\n\tconst sshHost = isIpv6Literal ? bareHost.slice(1, -1) : bareHost;\n\tconst username = url.username || undefined;\n\tconst port = url.port ? Number(url.port) : undefined;\n\tif (port === 0) {\n\t\tthrow new Error(\"ssh://: port 0 is not a valid SSH port; use ssh://host:<1-65535>/<path> or omit the port\");\n\t}\n\t// An empty port (`ssh://prod:/path`, `ssh://user@host:/path`, including\n\t// percent-encoded authority parts) parses cleanly with `url.port === \"\"`, so it\n\t// slips past the malformed-authority guard and would be read as \"no port\" —","sourceCodeStart":145,"sourceCodeEnd":181,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/ssh-protocol.ts#L145-L181","documentation":"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.","triggerScenarios":"`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`.","commonSituations":"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).","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."],"exampleFix":"// before\nawait readResource(`ssh://${user}@prod/etc/hosts`); // user = \"100%admin\"\n// after\nawait readResource(`ssh://${encodeURIComponent(user)}@prod/etc/hosts`);","handlingStrategy":"validation","validationCode":"function buildSshAuthority(user?: string, host: string): string {\n  const enc = (s: string) => encodeURIComponent(s);\n  return user ? `${enc(user)}@${host}` : host; // encode username; host must be a plain name/IP\n}","typeGuard":"function decodableAuthorityPart(part: string): boolean {\n  if (!part.includes(\"%\")) return true;\n  try { decodeURIComponent(part); return true; } catch { return false; }\n}","tryCatchPattern":"try {\n  return await sshHandler.resolve(url);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"invalid percent-escape in authority\")) {\n    // rebuild with encodeURIComponent on username; encode literal '%' as %25\n    return sshHandler.resolve(parseInternalUrl(`ssh://${encodeURIComponent(user)}@${host}/path`));\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["url","ssh-protocol","encoding","percent-encoding","authority"],"backgroundTag":"invalid-url-encoding","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}