{"record":{"id":"2cae7f80031cb999","repo":"can1357/oh-my-pi","slug":"imageurls-exposure-ssh-requires-imageurls-sshtar","errorCode":null,"errorMessage":"imageUrls exposure \"ssh\" requires imageUrls.sshTarget","messagePattern":"imageUrls exposure \"ssh\" requires imageUrls\\.sshTarget","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/exposure.ts","lineNumber":489,"sourceCode":"\t\t\t\tconst tunnelName = optionString(config, \"tunnelName\");\n\t\t\t\tif (!configFile || !tunnelName) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t'imageUrls exposure \"named-cloudflared\" requires credentials.tunnelToken or options.configFile and options.tunnelName',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\targv = [binary, \"tunnel\", \"--no-autoupdate\", \"--config\", configFile, \"run\", tunnelName];\n\t\t\t}\n\t\t\tconst baseUrl = normalizeBaseUrl(config.publicBaseUrl);\n\t\t\tconst { proc } = await spawnUrlTunnel(\n\t\t\t\targv,\n\t\t\t\t() => baseUrl,\n\t\t\t\t/Registered tunnel connection|Connection [a-z0-9-]+ registered/i,\n\t\t\t);\n\t\t\treturn processExposure(\"named-cloudflared\", baseUrl, proc);\n\t\t}\n\t\tcase \"ssh\": {\n\t\t\tif (!config.publicBaseUrl) throw new Error('imageUrls exposure \"ssh\" requires imageUrls.publicBaseUrl');\n\t\t\tif (!config.sshTarget) throw new Error('imageUrls exposure \"ssh\" requires imageUrls.sshTarget');\n\t\t\tconst binary = requireBinary(\"ssh\");\n\t\t\tconst remotePort = config.sshRemotePort ?? 8787;\n\t\t\tconst proc = Bun.spawn(\n\t\t\t\t[\n\t\t\t\t\tbinary,\n\t\t\t\t\t\"-o\",\n\t\t\t\t\t\"BatchMode=yes\",\n\t\t\t\t\t\"-o\",\n\t\t\t\t\t\"ExitOnForwardFailure=yes\",\n\t\t\t\t\t\"-N\",\n\t\t\t\t\t\"-R\",\n\t\t\t\t\t`${remotePort}:127.0.0.1:${port}`,\n\t\t\t\t\tconfig.sshTarget,\n\t\t\t\t],\n\t\t\t\t{ env: process.env, stdin: \"ignore\", stdout: \"ignore\", stderr: \"ignore\", cwd: os.homedir() },\n\t\t\t);\n\t\t\tconst early = await Promise.race([\n\t\t\t\tproc.exited.then(code => code),","sourceCodeStart":471,"sourceCodeEnd":507,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/exposure.ts#L471-L507","documentation":"The ssh exposure needs a destination for the reverse forward in `user@host[:port]` form (config.sshTarget). This error throws when kind is \"ssh\" but sshTarget is missing. Without it, there is no host to run `ssh -R` against.","triggerScenarios":"Selecting exposure kind \"ssh\" with publicBaseUrl set but no imageUrls.sshTarget; sshTarget set to an empty string.","commonSituations":"User assumed sshTarget comes from the machine's ssh config; switched exposure kind from a tunnel (ngrok etc.) to ssh without adding sshTarget; forgot the field when writing config by hand.","solutions":["Set imageUrls.sshTarget to \"user@host\" or \"user@host:port\".","Confirm passwordless/keyed ssh access: `ssh user@host true` succeeds non-interactively.","Alternatively pick a tunnel kind (cloudflared, ngrok, bore) that needs no ssh target."],"exampleFix":"// before\n\"imageUrls\": { \"exposure\": { \"kind\": \"ssh\" } }\n// after\n\"imageUrls\": { \"exposure\": { \"kind\": \"ssh\", \"sshTarget\": \"deploy@myserver.example.com\" } }","handlingStrategy":"validation","validationCode":"if (config.kind === \"ssh\") {\n  if (typeof config.sshTarget !== \"string\" || !/^[^@\\s]+@[^@\\s]+(:\\d+)?$/.test(config.sshTarget)) {\n    throw new Error('ssh exposure needs sshTarget like \"user@host\" or \"user@host:22\"');\n  }\n}","typeGuard":"function hasSshTarget(c: { kind: string; sshTarget?: string }): c is typeof c & { sshTarget: string } {\n  return c.kind !== \"ssh\" || (typeof c.sshTarget === \"string\" && /^[^@\\s]+@[^@\\s]+(:\\d+)?$/.test(c.sshTarget));\n}","tryCatchPattern":null,"preventionTips":["Provide sshTarget in user@host[:port] form","Test non-interactive ssh access (`ssh -o BatchMode=yes user@host true`) before configuring","Use an ssh config alias consistently between sshTarget and ~/.ssh/config"],"tags":["configuration","ssh","missing-required-option"],"backgroundTag":"missing-required-option","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}