paperclipai/paperclip · error · RailwayError

railway_target_mismatch

railway_target_mismatch

Error message

Invalid Railway container instance.

What it means

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.

Solutions

  1. Pass the full 36-character container instance UUID (e.g. '123e4567-e89b-12d3-a456-426614174000')
  2. Extract the instance id from the correct Railway API response field, not the service or project id
  3. Normalize: trim whitespace and, if sourcing from a URL, take the final UUID path segment
  4. Add client-side validation: /^[a-f0-9-]{36}$/i.test(instanceId) before calling

Example fix

// before
runRailwaySsh({ instanceId: deployment.serviceId })
// after
runRailwaySsh({ instanceId: deployment.meta?.containerInstanceId ?? deployment.id }) // full UUID
Defensive patterns

Strategy: validation

Validate before calling

const UUID_RE = /^[a-f0-9-]{36}$/i;
function assertInstanceId(id) {
  if (typeof id !== "string" || !UUID_RE.test(id)) {
    throw new TypeError(`instanceId must be a 36-char UUID, got: ${JSON.stringify(id)}`);
  }
  return id;
}

Type guard

function isRailwayInstanceId(v) {
  return typeof v === "string" && /^[a-f0-9-]{36}$/i.test(v);
}

Try / catch

try {
  const args = railwaySshArguments(dir, instanceId);
} catch (e) {
  if (e.code === "railway_target_mismatch") {
    logger.error({ instanceId }, "instanceId is not a container instance UUID");
    throw new UserInputError("Select a Railway container instance, not a service or project");
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/7acf1830212681eb. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/railway-ssh.ts:33

  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))) {
    throw new RailwayError("railway_ssh_host_key_invalid", "Paste verified known_hosts lines for ssh.railway.com only, without aliases, wildcards or comments.", 400);
  }
  return lines.join("\n") + "\n";
}

export async function generateRailwaySshKey(): Promise<{ publicKey: string; privateKey: string }> {
  const directory = await mkdtemp(path.join(tmpdir(), "paperclip-railway-key-"));
  try {
    const keyPath = path.join(directory, "identity");
    await promisify(execFile)("/usr/bin/ssh-keygen", ["-q", "-t", "ed25519", "-N", "", "-C", "paperclip-railway", "-f", keyPath], { timeout: 10_000, env: { PATH: "/usr/bin:/bin" } });
    return { publicKey: (await readFile(`${keyPath}.pub`, "utf8")).trim(), privateKey: await readFile(keyPath, "utf8") };
  } catch {
    throw new RailwayError("railway_ssh_unavailable", "Generating a Railway key requires system OpenSSH (ssh-keygen) on the Paperclip runtime.", 422);
  } finally { await rm(directory, { recursive: true, force: true }); }
}

export function railwaySshArguments(directory: string, instanceId: string): string[] {
  if (!/^[a-f0-9-]{36}$/i.test(instanceId)) throw new RailwayError("railway_target_mismatch", "Invalid Railway container instance.", 400);
  return [
    "-F", "/dev/null", "-T", "-i", path.join(directory, "identity"),
    "-o", "BatchMode=yes", "-o", "IdentitiesOnly=yes", "-o", "IdentityAgent=none",
    "-o", "ForwardAgent=no", "-o", "ClearAllForwardings=yes", "-o", "ControlMaster=no",
    "-o", "ControlPath=none", "-o", "PermitLocalCommand=no", "-o", "StrictHostKeyChecking=yes",
    "-o", `UserKnownHostsFile=${path.join(directory, "known_hosts")}`, "-o", "GlobalKnownHostsFile=/dev/null",
    "-o", "ConnectTimeout=10", "-o", "ServerAliveInterval=5", "-o", "ServerAliveCountMax=2",
    "--", `${instanceId}@ssh.railway.com`, "sh -s",
  ];
}

export async function runRailwaySshCommand(input: RailwaySshInput & { privateKey: string; knownHosts: string }) {
  input.signal.throwIfAborted();
  const knownHosts = validateRailwayKnownHosts(input.knownHosts);
  if (!input.privateKey.startsWith("-----BEGIN OPENSSH PRIVATE KEY-----")) throw new RailwayError("railway_ssh_key_invalid", "Regenerate the Railway connection's SSH key.", 422);
  const directory = await mkdtemp(path.join(tmpdir(), "paperclip-railway-command-"));
  try {
    await writeFile(path.join(directory, "identity"), input.privateKey, { mode: 0o600 });

View on GitHub (pinned to 3f1d897a7c)