paperclipai/paperclip · error · RailwayError

railway_ssh_unavailable

railway_ssh_unavailable

Error message

Generating a Railway key requires system OpenSSH (ssh-keygen) on the Paperclip runtime.

What it means

RailwayError (code railway_ssh_unavailable, HTTP 422) thrown by generateRailwaySshKey when spawning /usr/bin/ssh-keygen fails. Key generation depends on system OpenSSH being installed at that exact path on the Paperclip runtime; any spawn or execution error (missing binary, non-zero exit, timeout) is mapped to this error.

Solutions

  1. Install OpenSSH client in the runtime image (e.g. apk add openssh-client / apt-get install -y openssh-client)
  2. Verify the binary exists: ls -l /usr/bin/ssh-keygen; if it is elsewhere, symlink or adjust to /usr/bin
  3. Run the container/hosts with permission to exec /usr/bin binaries (no noexec mount, not fully read-only without the package)
  4. Check for 10s timeout being hit — retry once and confirm disk/CPU health

Example fix

// Dockerfile before
FROM node:20-slim
// after
FROM node:20-slim
RUN apt-get update && apt-get install -y --no-install-recommends openssh-client && rm -rf /var/lib/apt/lists/*
Defensive patterns

Strategy: fallback

Validate before calling

import { accessSync, constants } from "node:fs";
function sshKeygenAvailable() {
  try { accessSync("/usr/bin/ssh-keygen", constants.X_OK); return true; } catch { return false; }
}
if (!sshKeygenAvailable()) {
  // surface install instructions or fall back to user-supplied keys
}

Type guard

null

Try / catch

try {
  const { publicKey, privateKey } = await generateRailwaySshKey();
} catch (e) {
  if (e.code === "railway_ssh_unavailable") {
    showUserError("Install openssh-client in this environment, or paste your own ed25519 key.");
    return promptForManualKey();
  }
  throw e;
}

Prevention

When it happens

Trigger: generateRailwaySshKey runs but execFile('/usr/bin/ssh-keygen', ...) throws: binary not installed, installed at a different path (e.g. /bin/ssh-keygen or Homebrew /opt/homebrew/bin), not executable, ENOENT/EACCES, or exceeds the 10s timeout with the restricted PATH=/usr/bin:/bin.

Common situations: Minimal Docker images (distroless/alpine without openssh-client); macOS dev machines where ssh-keygen lives at /usr/bin but sandbox blocks execFile; hardened containers with noexec on /usr; slow disk causing the 10s timeout.

Understand the failure class

Background: "not installed", "pip install", "required for": how missing-dependency errors surface across open-source libraries — this error's family across 34 libraries.

Related errors


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

Appendix: source

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

export const RAILWAY_SSH_SECRET_PATH = "railway.ssh_private_key";

export function validateRailwayKnownHosts(value: string): string {
  if (value.length > 8192) throw new RailwayError("railway_ssh_host_key_invalid", "The Railway host key is too long.", 400);
  const lines = value.trim().split(/\r?\n/);
  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();

View on GitHub (pinned to 3f1d897a7c)