<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>purple - Terminal SSH Manager and SSH Config Editor in Rust</title>
<meta name="description" content="Open-source terminal SSH manager and ~/.ssh/config editor. Fuzzy search hundreds of hosts, sync AWS, GCP, Azure and 13 more clouds, transfer files, manage Docker and Podman over SSH, sign short-lived Vault SSH certs. Free Termius alternative in Rust.">
<meta name="keywords" content="ssh manager, ssh client, ssh config editor, terminal ssh manager, tui ssh, rust ssh tui, ratatui, termius alternative, sshs alternative, ssh bookmarks manager, cloud ssh sync, multi-cloud ssh inventory, ssh tunnel manager, sftp client, scp file transfer, docker over ssh, podman over ssh, agentless container management, portainer alternative, hashicorp vault ssh, short-lived ssh certificates, mcp server, model context protocol, claude code ssh, claude desktop ssh, mcpb bundle, ssh mcp server, ai agent ssh tool, mcp audit log, devops, sysadmin, homelab">
<meta name="robots" content="index, follow">
<meta name="author" content="Eric Kochen">
<meta name="color-scheme" content="dark">
<meta property="og:title" content="purple - Terminal SSH Manager and SSH Config Editor in Rust">
<meta property="og:description" content="Open-source terminal SSH manager and ~/.ssh/config editor. Fuzzy search hundreds of hosts, sync AWS, GCP, Azure and 13 more clouds, transfer files, manage Docker and Podman over SSH, sign short-lived Vault SSH certs. Free Termius alternative in Rust.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://getpurple.sh">
<meta property="og:image" content="https://raw.githubusercontent.com/erickochen/purple/master/preview.png">
<meta property="og:image:type" content="image/png">
<meta property="og:image:alt" content="purple terminal SSH manager showing host list with search, cloud sync and detail panel">
<meta property="og:image:width" content="1300">
<meta property="og:image:height" content="600">
<meta property="og:locale" content="en_US">
<meta property="og:site_name" content="purple">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="purple - Terminal SSH Manager and SSH Config Editor in Rust">
<meta name="twitter:description" content="Open-source terminal SSH manager and ~/.ssh/config editor. Fuzzy search hundreds of hosts, sync AWS, GCP, Azure and 13 more clouds, transfer files, manage Docker and Podman over SSH, sign short-lived Vault SSH certs. Free Termius alternative in Rust.">
<meta name="twitter:image" content="https://raw.githubusercontent.com/erickochen/purple/master/preview.png">
<link rel="canonical" href="https://getpurple.sh">
<link rel="alternate" hreflang="en" href="https://getpurple.sh">
<link rel="alternate" hreflang="x-default" href="https://getpurple.sh">
<link rel="alternate" type="text/plain" href="https://getpurple.sh/llms.txt" title="LLM context">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "purple",
"alternateName": "purple-ssh",
"description": "Open-source terminal SSH manager and SSH config editor written in Rust. Search, connect to and manage hundreds of SSH hosts from a single TUI. Syncs servers from 16 cloud providers including AWS, Azure, GCP, Hetzner, Proxmox and OCI, transfers files via scp, manages Docker and Podman containers over SSH, signs short-lived HashiCorp Vault SSH certificates and exposes an MCP server for AI agents. Edits ~/.ssh/config with round-trip fidelity.",
"applicationCategory": "DeveloperApplication",
"applicationSubCategory": "Terminal User Interface",
"operatingSystem": "macOS, Linux",
"url": "https://getpurple.sh",
"downloadUrl": "https://getpurple.sh",
"installUrl": "https://github.com/erickochen/purple/releases",
"softwareVersion": "2.45.0",
"datePublished": "2024-10-01",
"dateModified": "2026-04-19",
"softwareRequirements": "macOS or Linux",
"programmingLanguage": "Rust",
"license": "https://opensource.org/licenses/MIT",
"codeRepository": "https://github.com/erickochen/purple",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"author": {
"@type": "Person",
"name": "Eric Kochen",
"url": "https://github.com/erickochen"
},
"keywords": "SSH, SSH client, SSH server manager, Docker, Podman, container management, Docker TUI, Portainer alternative, SSH bookmarks, SSH launcher, TUI, terminal user interface, cloud sync, file transfer, DevOps, sysadmin, multi-cloud, open source",
"screenshot": "https://raw.githubusercontent.com/erickochen/purple/master/demo.gif",
"featureList": [
"SSH config round-trip fidelity",
"Fuzzy search across hosts",
"Host tagging and filtering",
"SSH tunnel management",
"Container management via SSH (Docker and Podman) with start, stop and restart",
"Command snippets with multi-host and parallel execution",
"Remote file explorer with dual-pane local/remote browsing and scp transfer",
"Cloud provider sync: AWS EC2, Azure, DigitalOcean, GCP (Compute Engine), Hetzner, i3D.net, Leaseweb, Linode (Akamai), Oracle Cloud Infrastructure (OCI), OVHcloud, Proxmox VE, Scaleway, Tailscale, TransIP, UpCloud, Vultr",
"Password management: OS Keychain, 1Password, Bitwarden, pass, HashiCorp Vault KV secrets engine, custom commands",
"Short-lived SSH certificates signed via the HashiCorp Vault SSH secrets engine",
"Bulk import from hosts files and known_hosts",
"SSH key management",
"Atomic writes with automatic backups",
"Split-pane detail panel with connection info, activity sparkline, provider metadata, tunnels and snippets",
"Shell completions for Bash, zsh, fish",
"MCP server for AI agent integration (Claude Code, Cursor): list hosts, run commands and manage containers via JSON-RPC 2.0 over stdio"
]
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "Install purple MCP server in Claude Desktop",
"description": "Install the purple .mcpb (MCP Bundle) in Claude Desktop for one-click access to your SSH hosts and containers.",
"totalTime": "PT2M",
"tool": [{ "@type": "HowToTool", "name": "purple .mcpb bundle" }, { "@type": "HowToTool", "name": "Claude Desktop" }],
"step": [
{
"@type": "HowToStep",
"position": 1,
"name": "Download the .mcpb bundle",
"text": "Visit https://github.com/erickochen/purple/releases/latest and download the .mcpb file matching your architecture (purple-X.Y.Z-aarch64-apple-darwin.mcpb for Apple Silicon, x86_64-apple-darwin for Intel Macs, x86_64-unknown-linux-gnu for Linux x64)."
},
{
"@type": "HowToStep",
"position": 2,
"name": "Double-click to install",
"text": "Double-click the downloaded .mcpb file. Claude Desktop opens an installer dialog showing the bundle metadata and asking for two paths: your SSH config file (default ~/.ssh/config) and the audit log path (default ~/.purple/mcp-audit.log). Click Install."
},
{
"@type": "HowToStep",
"position": 3,
"name": "Use the tools in Claude Desktop",
"text": "Restart Claude Desktop if needed. In a new conversation, ask things like 'list my SSH hosts' or 'show me details of host web-prod-1'. The bundle ships in --read-only mode so list_hosts, get_host and list_containers are available; run_command and container_action are blocked. For the full tool set, install purple via Homebrew or cargo and configure claude_desktop_config.json directly."
}
]
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is purple SSH?",
"acceptedAnswer": {
"@type": "Answer",
"text": "purple is a free, open-source terminal SSH client for managing SSH servers. It reads your ~/.ssh/config and gives you instant search, visual file transfer, command snippets, cloud sync from 16 providers and automatic password management. Single Rust binary for macOS and Linux."
}
},
{
"@type": "Question",
"name": "Is purple an SSH bookmark manager?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. purple stores every SSH host in ~/.ssh/config as a named bookmark, fuzzy-searches them by alias, hostname or tag, and connects on Enter. Frecency sorting keeps your most-used bookmarks on top. purple also syncs bookmarks from 16 cloud providers and signs short-lived Vault SSH certificates."
}
},
{
"@type": "Question",
"name": "Can I transfer files between local and remote servers with purple?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Press F on any host to open the remote file explorer. It shows local files on the left and the remote server on the right. Navigate directories, select files and copy them between machines via scp. Works through ProxyJump chains, password sources and active tunnels."
}
},
{
"@type": "Question",
"name": "What cloud providers does purple support?",
"acceptedAnswer": {
"@type": "Answer",
"text": "purple syncs servers from sixteen cloud providers: AWS EC2, Azure, DigitalOcean, GCP (Compute Engine), Hetzner, i3D.net, Leaseweb, Linode (Akamai), Oracle Cloud Infrastructure (OCI), OVHcloud, Proxmox VE, Scaleway, Tailscale, TransIP, UpCloud and Vultr. Each provider is configured with an API token or credentials profile. Synced hosts are tracked in your SSH config and updated on each sync."
}
},
{
"@type": "Question",
"name": "How do command snippets work in purple?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Save commands and run them on remote hosts via SSH. In the TUI, press r to run on the selected host, Ctrl+Space to multi-select hosts then r, or R to run on all visible hosts. The CLI alternative supports tag-based targeting (--tag prod) and parallel execution (--parallel). Snippets are stored locally in ~/.purple/snippets."
}
},
{
"@type": "Question",
"name": "How does SSH password management work in purple?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Set a password source per host via the TUI or a global default. When you connect, purple acts as SSH_ASKPASS and retrieves the password automatically. Supported sources: OS Keychain, 1Password, Bitwarden, pass, HashiCorp Vault KV secrets engine and custom commands. For short-lived SSH certificates purple also integrates with the HashiCorp Vault SSH secrets engine (a separate engine)."
}
},
{
"@type": "Question",
"name": "Can I manage Docker or Podman containers with purple?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Press C on any host to list all containers over SSH. Start, stop and restart without leaving the TUI. Purple auto-detects Docker or Podman on the remote host. No agent. No web UI. No extra ports."
}
},
{
"@type": "Question",
"name": "Does purple modify my existing SSH config?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Only when you add, edit, delete or sync. All writes are atomic with automatic backups. Auto-sync runs on startup for providers that have it enabled (configurable per provider)."
}
},
{
"@type": "Question",
"name": "Will purple break my SSH config comments or formatting?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No. purple preserves comments, indentation and unknown directives through every read-write cycle. Consecutive blank lines are collapsed to one."
}
},
{
"@type": "Question",
"name": "Does purple need a daemon or background process?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No. purple is a single Rust binary. Run it, use it, close it. No runtime, no daemon, no async framework."
}
},
{
"@type": "Question",
"name": "Does purple send my SSH config anywhere?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No. Your config never leaves your machine. Provider sync calls cloud APIs to fetch server lists. The TUI checks GitHub for new releases on startup (cached for 24 hours). No config data is transmitted."
}
},
{
"@type": "Question",
"name": "Can I use purple with SSH Include files?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Hosts from Include files are displayed in the TUI but never modified. purple resolves Include directives recursively (up to depth 16) with tilde and glob expansion."
}
},
{
"@type": "Question",
"name": "How do I sync Google Cloud (GCP) instances with purple?",
"acceptedAnswer": {
"@type": "Answer",
"text": "In the TUI, press S to open the provider list, then add GCP. Fill in your service account JSON key file path, project ID and optionally specific zones. Purple reads the key, creates a JWT and exchanges it for an access token automatically. The CLI alternative is purple provider add gcp --token /path/to/sa-key.json --project my-project."
}
},
{
"@type": "Question",
"name": "How do I sync Azure VMs with purple?",
"acceptedAnswer": {
"@type": "Answer",
"text": "In the TUI, press S to open the provider list, then add Azure. Fill in your service principal JSON file path and subscription IDs. Supports both az CLI and portal credential formats. The CLI alternative is purple provider add azure --token /path/to/sp.json --regions SUBSCRIPTION_ID."
}
},
{
"@type": "Question",
"name": "How do I sync Oracle Cloud Infrastructure (OCI) instances with purple?",
"acceptedAnswer": {
"@type": "Answer",
"text": "In the TUI, press S to open the provider list, then add Oracle. Fill in your OCI config file path, compartment OCID and regions. The CLI alternative is purple provider add oracle --token ~/.oci/config --compartment OCID --regions eu-amsterdam-1. Requires IAM policies: read instance-family and read virtual-network-family."
}
},
{
"@type": "Question",
"name": "How do I sync AWS EC2 instances with purple?",
"acceptedAnswer": {
"@type": "Answer",
"text": "In the TUI, press S to open the provider list, then add AWS. Select your regions from the region picker and fill in your credentials profile or access key. The CLI alternative is purple provider add aws --profile default --regions us-east-1,eu-west-1. EC2 tags are synced (excluding internal aws:* tags). AMI names are resolved for OS metadata."
}
},
{
"@type": "Question",
"name": "Is purple a Portainer alternative?",
"acceptedAnswer": {
"@type": "Answer",
"text": "For container visibility and basic lifecycle control (start, stop, restart) over SSH, yes. Press C on any host to see its containers. No agent to install, no web UI to host, no ports to open. Works with Docker and Podman. Purple does not provide container creation, registry management or role-based access control."
}
},
{
"@type": "Question",
"name": "How does purple compare to Lazydocker?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Lazydocker manages Docker locally on the host where it is installed. purple manages containers on remote servers over SSH from your local machine. Use Lazydocker for single-host local management. Use purple for multi-host remote management across your fleet."
}
},
{
"@type": "Question",
"name": "Can AI assistants use purple?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Run purple mcp to start a Model Context Protocol server over JSON-RPC 2.0. Claude Code, Cursor and other MCP-compatible AI agents can use five tools: list_hosts, get_host, run_command, list_containers and container_action. Pass --read-only to restrict to the safe tools (list_hosts, get_host, list_containers). Every call is logged to ~/.purple/mcp-audit.log by default. Claude Desktop users can install the .mcpb bundle from GitHub releases for a one-click setup that ships in --read-only mode."
}
},
{
"@type": "Question",
"name": "How do I troubleshoot connection problems?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Run with --verbose to enable debug logging, then purple logs --tail in another terminal. Logs are written to ~/.purple/purple.log with fault domain prefixes: [external] for remote/tool errors, [config] for local config issues. Set PURPLE_LOG=trace for maximum detail."
}
}
]
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"url": "https://getpurple.sh",
"name": "purple",
"description": "Open-source terminal SSH manager and SSH config editor in Rust"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "VideoObject",
"name": "purple Terminal SSH Client Demo",
"description": "Searching hosts, managing Docker containers, transferring files, connecting via SSH and syncing cloud providers in the terminal",
"thumbnailUrl": "https://raw.githubusercontent.com/erickochen/purple/master/demo.gif",
"contentUrl": "https://raw.githubusercontent.com/erickochen/purple/master/demo.webm",
"uploadDate": "2024-10-01",
"encodingFormat": "video/webm"
}
</script>
<style>
:root {
--bg: #0a0a14;
--bg-s: #0f0f1e;
--bg-t: #161628;
--fg: #e0d6f0;
--fg-2: #8878a8;
--fg-3: #3d3558;
--border: #2a2045;
--accent: #b44aff;
--accent-soft: rgba(180, 74, 255, 0.1);
--cyan: #00f0ff;
--cyan-soft: rgba(0, 240, 255, 0.08);
--magenta: #ff2a6d;
--green: #05ffa1;
--red: #ff2a6d;
--yellow: #f0e030;
--mono: "SF Mono", "Fira Code", "JetBrains Mono", "Cascadia Code", Menlo, Monaco, "Courier New", monospace;
--glow-accent: 0 0 20px rgba(180, 74, 255, 0.3), 0 0 60px rgba(180, 74, 255, 0.1);
--glow-cyan: 0 0 20px rgba(0, 240, 255, 0.3), 0 0 60px rgba(0, 240, 255, 0.1);
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
background: var(--bg);
color: var(--fg);
font-family: var(--mono);
font-size: 15px;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
/* ── Scanlines ── */
body::after {
content: "";
position: fixed;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 0, 0, 0.03) 2px,
rgba(0, 0, 0, 0.03) 4px
);
pointer-events: none;
z-index: 9999;
will-change: transform;
}
/* ── Animations ── */
@keyframes up {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
@keyframes glow-pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
/* ── Cursor ── */
.cursor {
display: inline-block;
width: 8px;
height: 1.1em;
background: var(--cyan);
vertical-align: text-bottom;
animation: blink 1s step-end infinite;
box-shadow: 0 0 8px var(--cyan);
}
.h1-cursor {
width: 0.06em;
height: 0.75em;
background: var(--accent);
margin-left: 0.04em;
vertical-align: baseline;
box-shadow: 0 0 12px var(--accent);
}
/* ── Terminal frame ── */
.terminal {
background: var(--bg-s);
border: 1px solid var(--border);
border-radius: 4px;
overflow: hidden;
width: 100%;
max-width: 640px;
margin: 0 auto;
box-shadow: var(--glow-accent), inset 0 1px 0 rgba(180, 74, 255, 0.1);
}
.terminal-bar {
padding: 10px 16px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
position: relative;
background: rgba(180, 74, 255, 0.03);
}
.terminal-dots { display: flex; gap: 7px; }
.terminal-dots span { width: 10px; height: 10px; border-radius: 50%; }
.dot-close { background: var(--magenta); box-shadow: 0 0 6px var(--magenta); }
.dot-min { background: var(--yellow); box-shadow: 0 0 6px var(--yellow); }
.dot-max { background: var(--green); box-shadow: 0 0 6px var(--green); }
.terminal-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 0.7rem;
color: var(--fg-3);
text-transform: uppercase;
letter-spacing: 0.1em;
}
.terminal-body { padding: 20px 20px 16px; text-align: left; }
/* ── Hero ── */
.hero {
min-height: 100svh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 24px;
position: relative;
overflow: hidden;
}
.hero::before {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(180deg, transparent 0%, var(--bg) 100%),
repeating-linear-gradient(
90deg,
transparent,
transparent 79px,
rgba(180, 74, 255, 0.07) 79px,
rgba(180, 74, 255, 0.07) 80px
),
repeating-linear-gradient(
0deg,
transparent,
transparent 79px,
rgba(180, 74, 255, 0.07) 79px,
rgba(180, 74, 255, 0.07) 80px
);
pointer-events: none;
mask-image: radial-gradient(ellipse 80% 70% at 50% 45%, black 0%, transparent 100%);
-webkit-mask-image: radial-gradient(ellipse 80% 70% at 50% 45%, black 0%, transparent 100%);
}
.hero::after {
content: "";
position: absolute;
width: 800px;
height: 800px;
border-radius: 50%;
background: radial-gradient(circle, rgba(180, 74, 255, 0.12) 0%, rgba(0, 240, 255, 0.04) 40%, transparent 70%);
top: 50%;
left: 50%;
transform: translate(-50%, -55%);
pointer-events: none;
}
.hero-inner {
text-align: center;
max-width: 1000px;
width: 100%;
position: relative;
z-index: 1;
}
.hero-inner > * {
opacity: 0;
animation: up 0.7s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.hero-inner > :nth-child(1) { animation-delay: 0s; }
.hero-inner > :nth-child(2) { animation-delay: 0.08s; }
.hero-inner > :nth-child(3) { animation-delay: 0.16s; }
h1 {
font-size: clamp(3.5rem, 10vw, 6rem);
font-weight: 700;
letter-spacing: -0.05em;
line-height: 1;
margin-bottom: 16px;
text-shadow: 0 0 80px rgba(180, 74, 255, 0.4), 0 0 160px rgba(180, 74, 255, 0.15);
}
h1 .dot { color: var(--cyan); text-shadow: 0 0 20px var(--cyan); }
/* ── Hero SVG logotype (Berkeley Mono Bold outlines, glyph-identical everywhere) ── */
.h1-logo {
/* viewBox spans from cap-top to descender-bottom (943 design units) so the
SVG block matches what line-height:1 rendered text used to occupy. No
extra margin correction needed — the subtitle sits naturally underneath. */
height: 1em;
width: auto;
display: block;
margin: 0 auto;
overflow: visible; /* let drop-shadow spill past the viewBox */
}
.h1-logo .logo-word { fill: currentColor; }
.h1-logo .logo-dot {
fill: var(--cyan);
filter: drop-shadow(0 0 20px var(--cyan));
}
.h1-logo .logo-cursor {
fill: var(--accent);
filter: drop-shadow(0 0 12px var(--accent));
animation: blink 1s step-end infinite;
transform-box: fill-box;
}
.h1-sub {
display: block;
font-size: clamp(0.9rem, 2vw, 1.1rem);
color: var(--fg-2);
font-weight: 400;
letter-spacing: 0.06em;
text-transform: uppercase;
margin-top: 16px;
}
.tagline {
font-size: clamp(0.9rem, 2vw, 1.1rem);
color: var(--fg-2);
margin-bottom: 48px;
font-weight: 400;
letter-spacing: -0.01em;
}
/* ── Install block ── */
.prompt-line {
display: flex;
align-items: center;
font-size: 0.85rem;
line-height: 1.6;
min-height: 1.6em;
}
.prompt-char { color: var(--cyan); margin-right: 8px; font-weight: 600; flex-shrink: 0; text-shadow: 0 0 8px var(--cyan); }
.typed-text { color: var(--fg); white-space: pre; }
.install-output { font-size: 0.8rem; color: var(--fg-2); line-height: 1.8; padding: 4px 0 0; }
.install-output .success { color: var(--green); font-weight: 600; text-shadow: 0 0 8px var(--green); }
.copy-inline { margin-left: auto; flex-shrink: 0; }
.copy-btn {
background: none;
border: 1px solid var(--border);
border-radius: 2px;
color: var(--fg-2);
padding: 4px 12px;
font-family: inherit;
font-size: 0.65rem;
cursor: pointer;
transition: all 0.25s;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.copy-btn:hover { border-color: var(--cyan); color: var(--cyan); box-shadow: var(--glow-cyan); }
.alt-installs {
font-size: 0.75rem;
color: var(--fg-3);
padding: 8px 0 0;
line-height: 1.7;
}
.prompt-char-dim { color: var(--fg-3); margin-right: 8px; }
/* ── Content ── */
.content {
max-width: 960px;
margin: 0 auto;
padding: 0 24px 80px;
}
/* ── Demo ── */
.demo {
width: 100%;
margin: 0 auto 100px;
}
.demo img, .demo video {
width: 100%;
height: auto;
border-radius: 4px;
border: 1px solid var(--border);
display: block;
box-shadow: var(--glow-accent);
}
/* ── Story ── */
.story {
max-width: 620px;
margin: 0 auto 100px;
color: var(--fg-2);
font-size: 0.9rem;
line-height: 1.75;
}
.story p { margin-bottom: 16px; }
.story p:last-child { margin-bottom: 0; }
.story code {
font-size: 0.85em;
background: var(--bg-s);
padding: 2px 6px;
border-radius: 2px;
border: 1px solid var(--border);
color: var(--cyan);
}
.story strong { color: var(--fg); font-weight: 600; }
/* ── Features ── */
.features {
display: grid;
grid-template-columns: 1fr;
gap: 0;
max-width: 620px;
margin: 0 auto 100px;
}
.feat {
display: grid;
grid-template-columns: 2.2em 1fr;
align-items: baseline;
padding: 14px 0;
border-bottom: 1px solid var(--border);
font-size: 0.85rem;
line-height: 1.6;
transition: background 0.25s;
}
.feat:first-child { border-top: 1px solid var(--border); }
.feat:hover { background: var(--accent-soft); }
.feat-icon { font-size: 1rem; line-height: 1.6; }
.feat-text { color: var(--fg-2); }
.feat-text strong { color: var(--fg); font-weight: 600; }
/* ── Providers ── */
.providers-section {
text-align: center;
margin-bottom: 100px;
}
.providers-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--fg-3);
margin-bottom: 16px;
}
.providers {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 6px;
list-style: none;
max-width: 700px;
margin: 0 auto;
}
.providers li {
background: var(--bg-s);
border: 1px solid var(--border);
border-radius: 2px;
padding: 5px 12px;
font-size: 0.72rem;
color: var(--fg-2);
transition: all 0.25s;
letter-spacing: 0.02em;
}
.providers li:hover {
border-color: var(--cyan);
color: var(--cyan);
background: var(--cyan-soft);
box-shadow: 0 0 12px rgba(0, 240, 255, 0.15);
}
/* ── Divider ── */
.divider {
border: none;
border-top: 1px solid var(--border);
margin: 0 0 100px;
box-shadow: 0 1px 12px rgba(180, 74, 255, 0.08);
}
/* ── FAQ (man page) ── */
.faq { margin-bottom: 80px; }
.faq-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--fg-3);
margin-bottom: 16px;
text-align: center;
}
.man-page {
background: var(--bg-s);
border: 1px solid var(--border);
border-radius: 4px;
padding: 20px 24px;
max-width: 700px;
margin: 0 auto;
box-shadow: inset 0 1px 0 rgba(180, 74, 255, 0.06);
}
.man-head, .man-foot {
display: flex;
justify-content: space-between;
font-size: 0.7rem;
color: var(--cyan);
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.man-head { margin-bottom: 16px; }
.man-foot { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--border); }
.man-page details {
border-bottom: 1px solid var(--border);
}
.man-page details:last-of-type { border-bottom: none; }
.man-page summary {
padding: 10px 0;
font-weight: 600;
font-size: 0.82rem;
cursor: pointer;
list-style: none;
display: flex;
justify-content: space-between;
align-items: center;
color: var(--fg);
transition: color 0.25s;
letter-spacing: -0.01em;
}
.man-page summary::-webkit-details-marker { display: none; }
.man-page summary:hover { color: var(--accent); text-shadow: 0 0 12px rgba(180, 74, 255, 0.3); }
.man-page summary::after {
content: "+";
font-size: 1rem;
color: var(--fg-3);
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), color 0.3s;
flex-shrink: 0;
margin-left: 16px;
}
.man-page details[open] summary::after {
transform: rotate(45deg);
color: var(--cyan);
text-shadow: 0 0 8px var(--cyan);
}
.man-page .answer {
font-size: 0.8rem;
color: var(--fg-2);
line-height: 1.7;
max-width: 600px;
padding-bottom: 12px;
}
.man-page .answer code {
font-size: 0.85em;
background: var(--bg);
padding: 2px 6px;
border-radius: 2px;
border: 1px solid var(--border);
color: var(--cyan);
}
/* ── CTA ── */
.cta {
text-align: center;
padding: 0 0 20px;
}
.cta-install {
background: var(--bg-s);
border: 1px solid var(--border);
border-radius: 4px;
padding: 14px 20px;
display: inline-flex;
align-items: center;
gap: 16px;
font-size: 0.9rem;
transition: all 0.25s;
margin-bottom: 20px;
}
.cta-install:hover { border-color: var(--accent); box-shadow: var(--glow-accent); }
.cta-install code { color: var(--fg); }
.dim { color: var(--fg-3); }
/* ── Footer ── */
footer {
text-align: center;
padding: 56px 24px 40px;
color: var(--fg-3);
font-size: 0.75rem;
letter-spacing: 0.02em;
}
footer a {
color: var(--fg-3);
text-decoration: none;
transition: color 0.25s;
}
footer a:hover { color: var(--cyan); }
footer .sep { margin: 0 0.3em; }
/* ── Responsive ── */
@media (max-width: 640px) {
body { font-size: 14px; }
.hero { min-height: 92svh; padding: 0 16px; }
.terminal { max-width: 100%; }
.terminal-body { padding: 16px 14px; }
.prompt-line { font-size: 0.75rem; }
.install-output { font-size: 0.7rem; }
.alt-installs { font-size: 0.65rem; }
.content { padding: 0 16px 60px; }
.demo { margin-bottom: 72px; }
.story { margin-bottom: 72px; }
.features { margin-bottom: 72px; }
.providers-section { margin-bottom: 72px; }
.divider { margin-bottom: 72px; }
.man-page { padding: 16px; }
.cta-install { padding: 12px 16px; gap: 12px; font-size: 0.8rem; }
}
@media (max-width: 480px) {
h1 { margin-bottom: 12px; }
.tagline { margin-bottom: 32px; }
.prompt-line { font-size: 0.7rem; }
.copy-inline { display: none; }
.cta-install { flex-direction: column; gap: 8px; font-size: 0.75rem; }
.man-head span:nth-child(2), .man-foot span:nth-child(2) { display: none; }
}
</style>
</head>
<body>
<div class="hero">
<div class="hero-inner">
<h1>
<!--
Hero logotype. Rendered once from Berkeley Mono Bold via fontTools so
every visitor sees the exact same glyphs regardless of installed fonts.
viewBox: unitsPerEm-space from the font (0..4200 horizontal, -245..956
vertical). Outer scale(1,-1) flips the font y-axis into SVG space.
To regenerate after a typeface change, re-run the fontTools export
script from the session notes; do not hand-edit path data.
-->
<svg class="h1-logo" viewBox="0 -728 4360 943" role="img" aria-label="purple.">
<g transform="scale(1,-1)">
<path class="logo-word" d="M80 -215H198V53H203C224 1 247 -10 332 -10C498 -10 534 39 534 261C534 488 496 538 325 538C240 538 217 527 198 475H193V528H80ZM304 97C212 97 192 127 192 264C192 401 212 431 304 431C397 431 418 401 418 264C418 127 397 97 304 97Z"/>
<path class="logo-word" d="M516 0V528H398V255C398 126 378 98 286 98C215 98 199 115 199 194V528H81V194C81 27 112 -10 254 -10C352 -10 380 6 408 80H413V0Z" transform="translate(600,0)"/>
<path class="logo-word" d="M156 0H273V219C273 382 303 418 442 418C472 418 494 416 538 409V524C472 530 447 531 399 530C308 529 300 524 245 421H240L256 528H156Z" transform="translate(1200,0)"/>
<path class="logo-word" d="M80 -215H198V53H203C224 1 247 -10 332 -10C498 -10 534 39 534 261C534 488 496 538 325 538C240 538 217 527 198 475H193V528H80ZM304 97C212 97 192 127 192 264C192 401 212 431 304 431C397 431 418 401 418 264C418 127 397 97 304 97Z" transform="translate(1800,0)"/>
<path class="logo-word" d="M85 0H528V101H368V728H95V627H257V101H85Z" transform="translate(2400,0)"/>
<path class="logo-word" d="M408 143C394 102 377 93 313 93C217 93 194 115 188 215H526C532 480 491 538 298 538C116 538 76 489 76 265C76 40 119 -10 315 -10C463 -10 500 18 519 143ZM189 305C193 415 214 439 303 439C390 439 411 415 416 305Z" transform="translate(3000,0)"/>
<path class="logo-dot" d="M224 0H376V152H224Z" transform="translate(3600,0)"/>
<rect class="logo-cursor" x="4240" y="0" width="80" height="680"/>
</g>
</svg>
<span class="h1-sub">One terminal. All your servers.</span>
</h1>
<h2 class="tagline">Open-source terminal SSH manager and SSH config editor for macOS and Linux. Search, connect, transfer files and manage containers. All from one TUI.</h2>
<div class="terminal">
<div class="terminal-bar">
<span class="terminal-dots">
<span class="dot-close"></span>
<span class="dot-min"></span>
<span class="dot-max"></span>
</span>
<span class="terminal-title">purple</span>
</div>
<div class="terminal-body">
<div class="prompt-line">
<span class="prompt-char">$</span>
<span class="typed-text" id="typed-cmd"></span><span class="cursor" id="typing-cursor"></span>
<button class="copy-btn copy-inline" id="copy-btn" onclick="copy(this)" style="display:none">copy</button>
</div>
<div class="install-output" id="install-output" style="display:none">
<div>Downloading purple v2.45.0 for darwin-arm64...</div>
<div>Installing to /usr/local/bin/purple... <span class="success">done.</span></div>
</div>
<div class="alt-installs" id="alt-installs" style="display:none">
<div><span class="prompt-char-dim">$</span> brew install erickochen/purple/purple</div>
<div><span class="prompt-char-dim">$</span> cargo install purple-ssh</div>
</div>
</div>
</div>
</div>
</div>
<main class="content">
<div class="demo">
<video autoplay loop muted playsinline
width="1920" height="900"
poster="https://raw.githubusercontent.com/erickochen/purple/master/demo.gif"
aria-label="purple terminal SSH client demo: searching hosts, managing containers, transferring files and syncing cloud providers">
<source src="https://raw.githubusercontent.com/erickochen/purple/master/demo.webm" type="video/webm">
<img src="https://raw.githubusercontent.com/erickochen/purple/master/demo.gif"
alt="purple terminal SSH client demo" loading="lazy" decoding="async" width="1920" height="900">
</video>
</div>
<div class="story">
<p>I had a perfectly good SSH config. Clean, well-organized, no complaints. That part worked.</p>
<p>What didn't work was the six other things I needed to do every day. Every container check was <code>ssh</code>, <code>docker ps</code>, scroll, repeat. Every file transfer was remembering <code>scp</code> flags. Every new cloud VM meant opening a console, copying an IP, editing my config by hand. And running the same command across a dozen hosts? That was either a bash loop or a whole Ansible setup for a one-liner.</p>
<p><strong>So I put all of it in one terminal.</strong></p>
</div>
<div class="features">
<div class="feat">
<span class="feat-icon">🔍</span>
<span class="feat-text"><strong>Find any host in a keystroke.</strong> Fuzzy matching across hostnames, IPs, tags and users. Your most-used servers float to the top automatically. Press <code>:</code> for a command palette with all 24 actions.</span>
</div>
<div class="feat">
<span class="feat-icon">☁️</span>
<span class="feat-text"><strong>Pull servers from 16 cloud providers.</strong> AWS, Azure, GCP, Hetzner, DigitalOcean, Proxmox VE, Tailscale and 9 more. New VMs sync in, IPs stay current, decommissioned hosts get flagged.</span>
</div>
<div class="feat">
<span class="feat-icon">🐳</span>
<span class="feat-text"><strong>See and control containers remotely.</strong> Docker and Podman over plain SSH. Start, stop, restart without installing anything on the remote.</span>
</div>
<div class="feat">
<span class="feat-icon">📂</span>
<span class="feat-text"><strong>Browse and copy files between machines.</strong> Dual-pane file explorer. Local filesystem on one side, remote on the other. Handles ProxyJump chains and tunnels.</span>
</div>
<div class="feat">
<span class="feat-icon">⚡</span>
<span class="feat-text"><strong>Run one command on many hosts.</strong> Pick a snippet, select your targets, execute. Results stream in per host.</span>
</div>
<div class="feat">
<span class="feat-icon">🔑</span>
<span class="feat-text"><strong>Passwords handled for you.</strong> Plugs into OS Keychain, 1Password, Bitwarden, pass, the HashiCorp Vault KV secrets engine or a custom script. Credentials are fetched at connect time.</span>
</div>
<div class="feat">
<span class="feat-icon">📜</span>
<span class="feat-text"><strong>Short-lived SSH certificates.</strong> Integrates with the HashiCorp Vault SSH secrets engine. Configure a role per host or per provider, press V to bulk-sign. Cached under ~/.purple/certs with automatic renewal.</span>
</div>
<div class="feat">
<span class="feat-icon">🤖</span>
<span class="feat-text"><strong>Let AI agents manage your servers.</strong> Built-in MCP server with one-click <code>.mcpb</code> install for Claude Desktop. Works with Claude Code, Cursor, Windsurf and any MCP-compatible agent. Read-only mode and a JSON Lines audit log are built in.</span>
</div>
<div class="feat">
<span class="feat-icon">📬</span>
<span class="feat-text"><strong>What's new overlay.</strong> Sticky toast and overlay summarizing releases since you last opened. Press n to reopen.</span>
</div>
</div>
<div class="providers-section">
<div class="providers-label">Cloud providers</div>
<ul class="providers">
<li>AWS EC2</li>
<li>Azure</li>
<li>DigitalOcean</li>
<li>GCP</li>
<li>Hetzner</li>
<li>i3D.net</li>
<li>Leaseweb</li>
<li>Linode</li>
<li>Oracle Cloud</li>
<li>OVHcloud</li>
<li>Proxmox VE</li>
<li>Scaleway</li>
<li>Tailscale</li>
<li>TransIP</li>
<li>UpCloud</li>
<li>Vultr</li>
</ul>
</div>
<hr class="divider">
<div class="faq">
<div class="faq-label">FAQ</div>
<div class="man-page">
<div class="man-head"><span>PURPLE(1)</span><span>General Commands Manual</span><span>PURPLE(1)</span></div>
<details>
<summary>Does purple modify my SSH config?</summary>
<div class="answer">Only when you explicitly add, edit, delete or sync. All writes are atomic with automatic backups. Comments, indentation and unknown directives are preserved.</div>
</details>
<details>
<summary>Does it need a daemon or background process?</summary>
<div class="answer">No. Single binary. Run it, use it, close it.</div>
</details>
<details>
<summary>Does it send my config anywhere?</summary>
<div class="answer">No. Your config never leaves your machine. Provider sync calls cloud APIs to fetch server lists. The TUI checks GitHub for new releases on startup (cached 24 hours). That's it.</div>
</details>
<details>
<summary>Can I manage Docker containers with purple?</summary>
<div class="answer">Yes. Press <code>C</code> on any host to list all containers over SSH. Start, stop, restart. Auto-detects Docker or Podman. No agent, no web UI, no extra ports.</div>
</details>
<details>
<summary>Can AI assistants use purple?</summary>
<div class="answer">Yes. Run <code>curl -fsSL getpurple.sh | sh</code>, then <code>purple mcp</code> to start the MCP server. Claude Code, Cursor, Windsurf and other agents get five tools: list_hosts, get_host, run_command, list_containers and container_action. Pass <code>--read-only</code> to restrict it to list_hosts, get_host and list_containers. Every call is logged to <code>~/.purple/mcp-audit.log</code> by default (JSON Lines, mode 0o600, run_command body redacted). Claude Desktop users can install the <code>.mcpb</code> bundle from <a href="https://github.com/erickochen/purple/releases/latest">GitHub releases</a> for a one-click setup. Full setup guide on the <a href="https://github.com/erickochen/purple/wiki/MCP-Server">wiki</a>.</div>
</details>
<details>
<summary>How do I troubleshoot connection problems?</summary>
<div class="answer">Run with <code>--verbose</code> to enable debug logging, then <code>purple logs --tail</code> in another terminal. Logs are written to <code>~/.purple/purple.log</code> with fault domain prefixes: <code>[external]</code> for remote/tool errors, <code>[config]</code> for local config issues. Set <code>PURPLE_LOG=trace</code> for maximum detail.</div>
</details>
<div class="man-foot"><span>purple v2.45.0</span><span>2026-04-19</span><span>PURPLE(1)</span></div>
</div>
</div>
<div class="cta">
<div class="cta-install">
<code><span class="dim">$</span> curl -fsSL getpurple.sh | sh</code>
<button class="copy-btn" onclick="copy(this)">copy</button>
</div>
</div>
</main>
<footer>
<a href="https://github.com/erickochen/purple" rel="noopener">GitHub</a> · <a href="https://github.com/erickochen/purple/wiki" rel="noopener">Docs</a> · <a href="https://crates.io/crates/purple-ssh" rel="noopener">crates.io</a> · MIT License · Rust · 6500+ tests
</footer>
<script>
function copy(btn) {
navigator.clipboard.writeText("curl -fsSL getpurple.sh | sh").then(function() {
btn.textContent = "copied";
setTimeout(function() { btn.textContent = "copy"; }, 2000);
}).catch(function() {});
}
(function() {
var cmd = "curl -fsSL getpurple.sh | sh";
var el = document.getElementById("typed-cmd");
var cursor = document.getElementById("typing-cursor");
var output = document.getElementById("install-output");
var copyBtn = document.getElementById("copy-btn");
var altInstalls = document.getElementById("alt-installs");
var i = 0;
function type() {
if (i < cmd.length) {
el.textContent += cmd[i];
i++;
setTimeout(type, 35 + Math.random() * 25);
} else {
setTimeout(function() {
cursor.style.display = "none";
copyBtn.style.display = "";
output.style.display = "block";
setTimeout(function() {
altInstalls.style.display = "block";
}, 300);
}, 400);
}
}
setTimeout(type, 1400);
})();
</script>
</body>
</html>