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
- 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
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
- 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
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
- railway_ssh_host_key_invalid
- CreateOS returned an invalid resource ID.
- device-login promotion: the account identifier cannot form…
- External chat interaction id is invalid
- github_webhook_recovery_invalid_input
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)