{"record":{"id":"7acf1830212681eb","repo":"paperclipai/paperclip","slug":"railway-target-mismatch","errorCode":"railway_target_mismatch","errorMessage":"Invalid Railway container instance.","messagePattern":"Invalid Railway container instance\\.","errorType":"error_code","errorClass":"RailwayError","httpStatus":400,"severity":"error","filePath":"server/src/services/railway-ssh.ts","lineNumber":33,"sourceCode":"  if (lines.length === 0 || lines.length > 5 || lines.some((line) => !/^ssh\\.railway\\.com (ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp256) [A-Za-z0-9+/]+={0,2}$/.test(line))) {\n    throw new RailwayError(\"railway_ssh_host_key_invalid\", \"Paste verified known_hosts lines for ssh.railway.com only, without aliases, wildcards or comments.\", 400);\n  }\n  return lines.join(\"\\n\") + \"\\n\";\n}\n\nexport async function generateRailwaySshKey(): Promise<{ publicKey: string; privateKey: string }> {\n  const directory = await mkdtemp(path.join(tmpdir(), \"paperclip-railway-key-\"));\n  try {\n    const keyPath = path.join(directory, \"identity\");\n    await promisify(execFile)(\"/usr/bin/ssh-keygen\", [\"-q\", \"-t\", \"ed25519\", \"-N\", \"\", \"-C\", \"paperclip-railway\", \"-f\", keyPath], { timeout: 10_000, env: { PATH: \"/usr/bin:/bin\" } });\n    return { publicKey: (await readFile(`${keyPath}.pub`, \"utf8\")).trim(), privateKey: await readFile(keyPath, \"utf8\") };\n  } catch {\n    throw new RailwayError(\"railway_ssh_unavailable\", \"Generating a Railway key requires system OpenSSH (ssh-keygen) on the Paperclip runtime.\", 422);\n  } finally { await rm(directory, { recursive: true, force: true }); }\n}\n\nexport function railwaySshArguments(directory: string, instanceId: string): string[] {\n  if (!/^[a-f0-9-]{36}$/i.test(instanceId)) throw new RailwayError(\"railway_target_mismatch\", \"Invalid Railway container instance.\", 400);\n  return [\n    \"-F\", \"/dev/null\", \"-T\", \"-i\", path.join(directory, \"identity\"),\n    \"-o\", \"BatchMode=yes\", \"-o\", \"IdentitiesOnly=yes\", \"-o\", \"IdentityAgent=none\",\n    \"-o\", \"ForwardAgent=no\", \"-o\", \"ClearAllForwardings=yes\", \"-o\", \"ControlMaster=no\",\n    \"-o\", \"ControlPath=none\", \"-o\", \"PermitLocalCommand=no\", \"-o\", \"StrictHostKeyChecking=yes\",\n    \"-o\", `UserKnownHostsFile=${path.join(directory, \"known_hosts\")}`, \"-o\", \"GlobalKnownHostsFile=/dev/null\",\n    \"-o\", \"ConnectTimeout=10\", \"-o\", \"ServerAliveInterval=5\", \"-o\", \"ServerAliveCountMax=2\",\n    \"--\", `${instanceId}@ssh.railway.com`, \"sh -s\",\n  ];\n}\n\nexport async function runRailwaySshCommand(input: RailwaySshInput & { privateKey: string; knownHosts: string }) {\n  input.signal.throwIfAborted();\n  const knownHosts = validateRailwayKnownHosts(input.knownHosts);\n  if (!input.privateKey.startsWith(\"-----BEGIN OPENSSH PRIVATE KEY-----\")) throw new RailwayError(\"railway_ssh_key_invalid\", \"Regenerate the Railway connection's SSH key.\", 422);\n  const directory = await mkdtemp(path.join(tmpdir(), \"paperclip-railway-command-\"));\n  try {\n    await writeFile(path.join(directory, \"identity\"), input.privateKey, { mode: 0o600 });","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/railway-ssh.ts#L15-L51","documentation":"RailwayError (code railway_target_mismatch, HTTP 400) thrown by railwaySshArguments when instanceId does not match a UUID (36 chars of hex and hyphens, case-insensitive). The SSH target must be a Railway container instance ID; anything else is rejected before any connection is attempted to prevent executing ssh against an arbitrary string.","triggerScenarios":"Calling railwaySshArguments (via args or child) with a non-UUID instanceId: an empty string, a service name, a shortened id, an environment slug, or a URL fragment instead of the full container instance UUID.","commonSituations":"Passing the Railway service ID instead of the deployment/container instance ID; trimming the UUID incorrectly; pulling the id from the wrong API field (projectId vs instanceId); user pasting a deployment URL where only the last path segment is the instance id.","solutions":["Pass the full 36-character container instance UUID (e.g. '123e4567-e89b-12d3-a456-426614174000')","Extract the instance id from the correct Railway API response field, not the service or project id","Normalize: trim whitespace and, if sourcing from a URL, take the final UUID path segment","Add client-side validation: /^[a-f0-9-]{36}$/i.test(instanceId) before calling"],"exampleFix":"// before\nrunRailwaySsh({ instanceId: deployment.serviceId })\n// after\nrunRailwaySsh({ instanceId: deployment.meta?.containerInstanceId ?? deployment.id }) // full UUID","handlingStrategy":"validation","validationCode":"const UUID_RE = /^[a-f0-9-]{36}$/i;\nfunction assertInstanceId(id) {\n  if (typeof id !== \"string\" || !UUID_RE.test(id)) {\n    throw new TypeError(`instanceId must be a 36-char UUID, got: ${JSON.stringify(id)}`);\n  }\n  return id;\n}","typeGuard":"function isRailwayInstanceId(v) {\n  return typeof v === \"string\" && /^[a-f0-9-]{36}$/i.test(v);\n}","tryCatchPattern":"try {\n  const args = railwaySshArguments(dir, instanceId);\n} catch (e) {\n  if (e.code === \"railway_target_mismatch\") {\n    logger.error({ instanceId }, \"instanceId is not a container instance UUID\");\n    throw new UserInputError(\"Select a Railway container instance, not a service or project\");\n  }\n  throw e;\n}","preventionTips":["Pass the container/deployment instance UUID, never the service id, project id, or URL slug","Normalize and trim the id from Railway API responses before use","Validate with /^[a-f0-9-]{36}$/i at the UI/CLI boundary before calling the service","When parsing from a Railway URL, take only the final UUID segment"],"tags":["validation","ssh","identifier","railway"],"backgroundTag":"invalid-identifier-format","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}