{"record":{"id":"d55a4abb41791b35","repo":"can1357/oh-my-pi","slug":"ssh-password-authentication-is-not-supported","errorCode":null,"errorMessage":"ssh://: password authentication is not supported; ssh:// uses key/agent auth — drop the ':<password>' from the URL","messagePattern":"ssh://: password authentication is not supported; ssh:// uses key/agent auth — drop the ':<password>' from the URL","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/ssh-protocol.ts","lineNumber":168,"sourceCode":"\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\" —\n\t// silently using the default/configured target. `url.rawHost` is the decoded\n\t// authority and uniquely retains the trailing `:`; comparing it to the decoded\n\t// host (+ user) catches the empty port, while a percent-encoded alias like\n\t// `prod%3A` (whose decoded host already ends in `:`) reconstructs to `prod::`\n\t// and is left alone.","sourceCodeStart":150,"sourceCodeEnd":186,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/ssh-protocol.ts#L150-L186","documentation":"The ssh:// protocol handler refuses any URL that embeds a password (`ssh://user:pass@host/path`). SSH authenticates with keys or an agent, never a URL password, so embedding one indicates the author ported an https://-style URL. The handler throws immediately during target resolution, before any connection is attempted.","triggerScenarios":"Passing a URL whose WHATWG parse yields a non-empty `url.password`, e.g. `ssh://deploy:secret@prod.example.com/etc/hosts` — typically a paste from an HTTPS git URL or a database connection string.","commonSituations":"Converting an `https://user:token@host/repo` URL to ssh://; pasting credentials from a legacy tool that supported password auth; mistaking ssh:// for a scheme that accepts inline credentials.","solutions":["Remove the `:<password>` from the URL, keeping `ssh://user@host/path`","Set up key-based auth (ssh-keygen + ssh-copy-id) or an ssh-agent for the host","Add the host to ~/.ssh/config or the project's ssh.json capability with keyPath instead of embedding credentials"],"exampleFix":"// before\nresolve('ssh://deploy:hunter2@prod.example.com/etc/hosts')\n// after\nresolve('ssh://deploy@prod.example.com/etc/hosts')","handlingStrategy":"validation","validationCode":"const u = new URL(candidate);\nif (u.protocol === 'ssh:' && u.password) throw new Error(`strip ':<password>' from ${candidate}; ssh uses key/agent auth`);","typeGuard":"function hasNoSshPassword(u: URL): boolean { return u.protocol !== 'ssh:' || !u.password; }","tryCatchPattern":"try {\n  const res = await handler.resolve(url, ctx);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('ssh://: password authentication is not supported')) {\n    // rebuild URL without credentials and retry\n  } else throw e;\n}","preventionTips":["Never embed credentials in ssh:// URLs — rely on ~/.ssh/config or ssh.json keyPath entries","Reuse ssh:// URLs derived from git remotes only after converting https:// credential URLs","Lint URL templates that interpolate `${password}` for the ssh scheme"],"tags":["ssh","authentication","url-validation"],"backgroundTag":"ssh-password-auth-not-supported","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}