<!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 Tunnel Monitor in Rust</title>
<meta name="description" content="Open-source terminal SSH manager in Rust. Keeps ~/.ssh/config in sync with your cloud infra across 16 cloud providers. Fuzzy host search, live tunnel monitoring, scp, Docker over SSH.">
<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, live ssh tunnel monitoring, ssh tunnel dashboard, real-time ssh tunnel, ssh tunnel throughput, ssh swimlane, 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 Tunnel Monitor in Rust">
<meta property="og:description" content="Open-source terminal SSH manager in Rust. Keeps ~/.ssh/config in sync with your cloud infra across 16 cloud providers. Fuzzy host search, live tunnel monitoring, scp, Docker over SSH.">
<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, live tunnel monitoring and detail panel">
<meta property="og:image:width" content="3264">
<meta property="og:image:height" content="1960">
<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 Tunnel Monitor in Rust">
<meta name="twitter:description" content="Open-source terminal SSH manager in Rust. Keeps ~/.ssh/config in sync with your cloud infra across 16 cloud providers. Fuzzy host search, live tunnel monitoring, scp, Docker over SSH.">
<meta name="twitter:image" content="https://raw.githubusercontent.com/erickochen/purple/master/preview.png">
<meta name="twitter:image:alt" content="purple terminal SSH manager showing host list with search, cloud sync, live tunnel monitoring and detail panel">
<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. Keeps ~/.ssh/config in sync with your cloud infra: spin up a VM on AWS, Azure, GCP, Hetzner, Proxmox, OCI or 10 other cloud providers and it appears in your host list, destroy it and the entry dims. No more hand-editing ssh config after every Terraform run. Search, connect to and manage hundreds of SSH hosts from a single TUI. Live SSH tunnel monitoring shows throughput, channel events and the apps moving bytes for every active forward (Local, Remote, Dynamic SOCKS) on a dedicated Tunnels page reachable with Tab. 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": "3.4.0",
"datePublished": "2024-10-01",
"dateModified": "2026-05-05",
"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, SSH tunnel monitor, live SSH tunnel monitoring, real-time SSH tunnel, SSH tunnel dashboard, SSH tunnel throughput, SSH swimlane, 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",
"Live SSH tunnel monitoring with throughput, channel events and per-client process roster",
"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": "HowTo",
"name": "How to monitor live SSH tunnels in purple",
"description": "Watch every active SSH forward across your hosts with live throughput, channel events and the apps moving bytes, on a dedicated Tunnels page in purple's TUI.",
"totalTime": "PT1M",
"tool": [{ "@type": "HowToTool", "name": "purple" }],
"step": [
{
"@type": "HowToStep",
"position": 1,
"name": "Open the Tunnels page",
"text": "Launch purple. From the host list press Tab to switch to the Tunnels page. Every LocalForward, RemoteForward and DynamicForward directive across all hosts in your ~/.ssh/config appears in one sortable, searchable list. Press Tab again to return to the host list."
},
{
"@type": "HowToStep",
"position": 2,
"name": "Select an active tunnel",
"text": "Use j and k to move the cursor. Active tunnels show a green dot, current throughput in bytes per second, uptime and last connect time. The detail panel opens automatically on the right whenever the cursor lands on an active tunnel."
},
{
"@type": "HowToStep",
"position": 3,
"name": "Read the live detail panel",
"text": "The detail panel shows three things in real time. Top: a per-client roster with PID, source port, age, current bps and a sparkline of recent throughput per client process, plus the responsible app the OS reports (Ghostty, Safari, Chrome, DataGrip). Middle: a 60-second swimlane of every channel opened and closed in the window (Direct channels for Local/Remote forwards, Dynamic channels for SOCKS). Bottom: uptime, peak concurrent channels, total opens, current and peak rx/tx counters."
},
{
"@type": "HowToStep",
"position": 4,
"name": "Sort, search or manage",
"text": "Press s to cycle sort (most recent uptime / alphabetical by host). Press / to search by alias or forward (bind to remote, bind to any for SOCKS). Press a to add a new tunnel from this page (a fuzzy host picker filters editable hosts as you type, then opens the tunnel form). Press e to edit, d to delete, Enter to start or stop the selected tunnel."
}
]
}
</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 keeps it in sync with your cloud infra across 16 cloud providers, so new VMs appear in your host list and decommissioned hosts dim without manual edits. Plus instant search, visual file transfer, command snippets and automatic password management. Single Rust binary for macOS and Linux."
}
},
{
"@type": "Question",
"name": "How does purple show live SSH tunnel activity?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Press Tab from the host list to switch to the dedicated Tunnels page. Every SSH forward across all your hosts shows up in one sortable, searchable list with current throughput, uptime and a green dot when active. The detail panel on the right opens automatically for active tunnels and shows: a per-client process roster (PID, source port, age, current bytes per second, sparkline of recent throughput, responsible app reported by the OS), a 60-second swimlane of channel opens and closes (Direct channels for LocalForward and RemoteForward, Dynamic channels for SOCKS), and uptime, peak concurrent and total opens counters. Sort with s (most recent / alphabetical), search with /, add a tunnel with a (host picker filters as you type) or use T from the host list for the per-host tunnel overlay."
}
},
{
"@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 keeps your bookmarks in sync with 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 keeps ~/.ssh/config in sync with 16 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. Drop in one API token per provider. New VMs appear in your host list automatically. Decommissioned hosts dim. No manual ssh config edits."
}
},
{
"@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;
max-width: 56ch;
margin-inline: auto;
text-wrap: balance;
}
/* ── 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 for macOS and Linux that keeps ~/.ssh/config in sync with your cloud infra. Spin up a VM, it shows up. Destroy it, it dims. Across 16 cloud providers.</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 v3.4.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><span class="prompt-char-dim">$</span> nix profile install github:erickochen/purple</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>Your ssh config tracks your infra.</strong> Spin up a VM on AWS, Azure, GCP, Hetzner, Proxmox VE, Tailscale or 10 other cloud providers and it shows up in your host list. Destroy it and the entry dims. No more hand-editing after every Terraform run.</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>Watch your SSH tunnels in real time.</strong> Dedicated Tunnels page with live throughput, channel events and the apps moving bytes. Local, Remote and Dynamic SOCKS, all in one sortable view. <code>Tab</code> from the Hosts page.</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 v3.4.0</span><span>2026-05-05</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 · 6800+ 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>