{"record":{"id":"986633a01fb68fc7","repo":"can1357/oh-my-pi","slug":"ssh-does-not-support-url-query-strings-percent","errorCode":null,"errorMessage":"ssh:// does not support URL query strings; percent-encode a literal '?' as %3F in the path: ${url.href}","messagePattern":"ssh:// does not support URL query strings; percent-encode a literal '\\?' as %3F in the path: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/ssh-protocol.ts","lineNumber":79,"sourceCode":"\t\treturn new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Remote absolute path from the URL. Uses `rawPathname` (pre-normalization) so\n * `..`/`//` and percent-escapes survive verbatim to the remote shell; the\n * authority (host/user/port) stays on the WHATWG fields, which preserve case for\n * the non-special `ssh` scheme.\n */\nfunction remotePathFromUrl(url: InternalUrl): string {\n\t// `?`/`#` are URL delimiters, so parseInternalUrl strips them from the path\n\t// (`ssh://h/tmp/a?draft` → `/tmp/a`). Reject the unsupported suffix instead of\n\t// silently operating on the truncated path; a literal `?`/`#` in a filename\n\t// must be percent-encoded (`%3F`/`%23`).\n\tif (url.search) {\n\t\tthrow new Error(\n\t\t\t`ssh:// does not support URL query strings; percent-encode a literal '?' as %3F in the path: ${url.href}`,\n\t\t);\n\t}\n\tif (url.hash) {\n\t\tthrow new Error(\n\t\t\t`ssh:// does not support URL fragments; percent-encode a literal '#' as %23 in the path: ${url.href}`,\n\t\t);\n\t}\n\tconst raw = url.rawPathname ?? url.pathname;\n\tlet decoded: string;\n\ttry {\n\t\tdecoded = decodeURIComponent(raw);\n\t} catch {\n\t\tthrow new Error(`Invalid URL encoding in ssh:// path: ${url.href}`);\n\t}\n\tif (!decoded) {\n\t\tthrow new Error(\n\t\t\t\"ssh:// requires an absolute path, e.g. ssh://host/etc/hosts or ssh://host/ for the root directory\",","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/ssh-protocol.ts#L61-L97","documentation":"In a URL, `?` starts a query string, so the internal-URL parser strips anything after it from the path (`ssh://h/tmp/a?draft` parses with pathname `/tmp/a` and search `?draft`). Since ssh:// remote paths are pure filesystem paths with no query semantics, the handler rejects any URL containing a query string rather than silently reading the truncated path.","triggerScenarios":"`SshProtocolHandler.resolve()` or `.write()` (via `remotePathFromUrl`) with a `ssh://` URL whose `url.search` is non-empty, e.g. `ssh://host/tmp/a?draft`, or any path whose filename contains a literal `?` that was not percent-encoded as `%3F`.","commonSituations":"Files with `?` in their names (legal on POSIX) passed to ssh:// unencoded; URL templates appending `?version=...` or cache-busting params by habit from HTTP URLs; tooling auto-appending query parameters.","solutions":["Percent-encode the literal `?` as `%3F` in the path: `ssh://host/tmp/a%3Fdraft`.","Remove any query string you appended out of HTTP habit — ssh paths take no query parameters.","Pass the intended remote path exactly; if you meant to select a different file, use that file's real path."],"exampleFix":"// before\nawait readResource(\"ssh://host/tmp/report?draft\");\n// after\nawait readResource(\"ssh://host/tmp/report%3Fdraft\");","handlingStrategy":"validation","validationCode":"function assertNoSshQuery(url: string): void {\n  const u = new URL(url);\n  if (u.protocol === \"ssh:\" && u.search) {\n    throw new Error(`ssh:// URL must not contain a query string: ${url}`);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await sshHandler.resolve(url);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"does not support URL query strings\")) {\n    // retry with '?' encoded\n    return sshHandler.resolve(parseInternalUrl(url.replaceAll(\"?\", \"%3F\")));\n  }\n  throw err;\n}","preventionTips":["Treat ssh:// as a filesystem path, not an HTTP URL — no query strings or fragments.","Always `encodeURIComponent()` path segments built from variables.","Encode literal '?' as %3F in filenames.","Never append cache-busting or version query params to ssh:// URLs."],"tags":["url","ssh-protocol","encoding"],"backgroundTag":"invalid-url-syntax","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}