Skip to content
mcglothiPublic

About

A fast, beautiful, native macOS workbench for people who live on SSH.

Topics

Resources

Contributing

Security policy

Stars

3 stars

Watchers

1 watching

Forks

Repository files navigation

⚓ Portside

A fast, native macOS workbench for people who live in remote terminals.

Portside is a session manager and terminal for operators who run fleets of Linux servers — plus their containers, Kubernetes pods, serial consoles, and legacy telnet endpoints — from a Mac. Connect over OpenSSH or mosh, plug into a switch with a native serial bridge, and keep every target in one searchable, foldered library. Multi-host broadcast with guardrails, SFTP, port forwarding, and credential glue all live in the same native window. No Electron, no web view: SwiftUI and a real terminal, built around the tools you already use.

Early days, moving fast. Built on the coast of Maine. ⛵

Install

Grab the latest notarized build from Releases, or:

brew install mcglothi/tap/portside

Builds are Developer ID signed and notarized by Apple, and keep themselves current via Sparkle. Requires macOS 14+ on Apple silicon — Intel Macs aren't supported. If that matters to you, please open an issue.

Portside's foldered session library with transport and environment badges

One foldered library for SSH and mosh hosts, containers, Kubernetes pods, serial consoles, and telnet endpoints — with transport and environment badges.

Quick Connect — a fuzzy-search command palette across the whole library

⌘K Quick Connect fuzzy-searches every saved target and recent connection at once.

A live SSH session in a native terminal tab, inspecting a deploy on a remote host

Real terminal tabs over the OpenSSH you already have configured — keys, agent, and ProxyJump chains included.

MultiExec broadcasting uptime to four of six panes; the two nonprod panes are excluded and dimmed, each pane carries a Broadcasting or Excluded status bar, and the banner offers Include All, Exclude All and Invert Selection

MultiExec broadcasts your keystrokes to every included pane — with a loud banner counting what's live. Drop a host out for a single command and put it back (⌥⌘M, or the status bar under any pane) without disarming.

The SFTP file browser riding the same SSH session as the terminal, listing a remote directory

The SFTP browser rides the same SSH connection as your shell — no second login — with drag-and-drop transfer right beside the terminal.

Why

macOS has excellent terminal emulators — iTerm2 and WezTerm are superb. What it lacks is a native operator workflow: a session library you can organize, broadcast execution across a fleet with real safety UX, credential glue, file transfer, and container/Kubernetes access — in one fast surface. Portside exists to fill that gap without giving up native speed and macOS polish.

Opinionated bets:

  • Win on workflow density, not protocol count. SSH-first, with the console transports operators still need close at hand.
  • Multi-host execution deserves safety UX as a flagship feature, not a checkbox.
  • Local-first, self-owned state. Your session library is a JSON file on your disk, not a cloud sync subscription.
  • Lean on OpenSSH itself for transport — your ~/.ssh/config, agent, keys, and ProxyJump chains work on day one, unmodified.

What works today

  • Session library with folders — organize hosts under prod, nonprod, personal, nested as deep as you like. Everything is editable in place.
  • Containers & Kubernetes as first-class sessions — save a docker/podman container, or a Kubernetes workload or pod through kubectl or oc (NKP, GKE, OpenShift, any kubeconfig, including a per-cluster file), the same way you save a host. It runs on this Mac or through an SSH jump host, and shows up in the library, Quick Connect, and recents like everything else. Browse contexts, workloads (deploy/web survives rollouts), pods and containers from the editor so you never have to remember a churning name. When a cluster sign-in has expired, the session says so and offers the platform's own Sign In — Portside never holds a token.
  • ~/.ssh/config import — seeds the library on first launch (follows Include directives); re-import merges new hosts anytime.
  • Import from MobaXterm — bring over .mxtsessions / .mxtmacros files with their folder structure intact. (Macros imported before 0.17 could pick up literal key names — yumSPACEupdate — from MobaXterm's keystroke format. Scripts/repair_moba_macros.py fixes those in place; re-importing does not, since import skips macros whose name already exists.)
  • Export & import — back up or move your library as portable JSON; sessions (with folders) and macros export separately and re-import into any Portside install. Passwords stay in the Keychain and never travel.
  • Native terminal tabs — SwiftTerm rendering across OpenSSH, mosh, serial, and telnet sessions. Local shells too (⌘T). Find in scrollback with ⌘F, per-tab zoom with ⌘+/−/0. See the tested compatibility matrix for exactly what's supported.
  • Terminal appearance & behavior — pick from hundreds of built-in themes and Nerd Fonts (or import your own), set a configurable scrollback depth (default 10,000 lines), and optionally switch on GPU (Metal) rendering — all in Settings, applied live to open terminals.
  • Inline images — imgcat a screenshot, a Grafana export, or a plot from a remote host and look at it where you are, instead of copying it back first. Sixel, iTerm2 (OSC 1337) and Kitty graphics all render; the compatibility matrix has the details. Try it without installing anything: cat docs/demo/portside-logo.six.
  • Mosh roaming — opt any SSH host into mosh for sessions that survive sleep and network changes. Portside respects your SSH alias, key, port, and saved password during bootstrap, and falls back to SSH when mosh is unavailable.
  • Native serial consoles — connect directly to live /dev/cu.* devices without spawning screen. Choose baud rate, data bits, parity, stop bits, and flow control; logging, run-on-connect, and MultiExec work on the same path.
  • Telnet for legacy endpoints — save host and port, handle RFC 854 option negotiation cleanly, and use the same terminal, logging, and MultiExec tools. Telnet sessions carry a prominent UNENCRYPTED badge.
  • Field filters — the sidebar filter takes env:prod, kind:k8s, folder:lab, profile:none, is:fav, - to exclude and /regex/; the ? beside the field lists them all, typing env: suggests values, non-matching hosts dim rather than vanish, and filters can be saved.
  • ssh:// and portside://connect/<name> links — open hosts from a wiki, dashboard or script. Unknown hosts always ask first; Settings ▸ Connection can make Portside the default ssh:// handler.
  • Quick Connect (⌘K) — a fuzzy-search command palette over the whole library; empty query lists recent hosts so it doubles as fast reconnect.
  • MultiExec — tile every session in a grid and type into all of them at once. Arming it is one step: with several separate tabs open it gathers them into Grid View automatically instead of requiring that first. A broadcast command bar for deliberate one-shot commands, and a loud orange banner so you always know when you're armed and how many panes are live. Every pane carries a Broadcasting / Excluded status bar so you can drop a host out for one command and put it back (⌥⌘M), with Include All (⌥⌘A), Exclude All (⌥⌘E) and Invert Selection (⌥⌘I) one click or one key away in the banner.
  • Environment badges & protected hosts — tag sessions prod / staging / dev / personal for color-coded badges in the sidebar and tabs. Protected hosts stay out of MultiExec by default and require explicit confirmation to join a broadcast.
  • Macros — named command sequences, run in the active terminal or across the whole MultiExec grid. Per-host run-on-connect commands too.
  • SFTP browser — per-session remote file pane riding the same SSH connection (no re-auth), with drag/drop upload and drag-out download (a persistent hint keeps that discoverable), automatic refresh when you switch hosts, and a confirmation before deleting anything. It follows cd in the live shell for bash/zsh (via the OSC 7 "current directory" escape sequence) — one-click "Install Shell Integration" appends the needed one-liner to the host's .bashrc/.zshrc over ssh (idempotent, with an option to source it into the session immediately), and auto-detects which shell you're running so you don't have to know.
  • Inventory coverage — for a large imported library, a view of what Portside doesn't know about your fleet: hosts with no environment tag, no credential profile, or no stored credentials, each fixable in bulk from the same place. Deliberately framed as coverage rather than errors — a host authenticating through ssh-agent isn't misconfigured, and the view says so.
  • Bulk classification — tag environment (prod/staging/dev/personal) or apply a credential profile across a multi-selection or a whole folder.
  • History — per-host connection totals are kept automatically and rank Quick Connect by frecency, so a host you use constantly outranks one touched once yesterday; hosts you haven't touched in months surface in Coverage. Optionally, a full log of every connection attempt with its outcome, and (opt-in) a record of every command run with its timing and exit code, taken from OSC 133 shell integration. Selecting a command shows the surrounding session transcript, so history reads as a table of contents for your logs. All of it is governed from one Settings ▸ Recording pane with a single privacy rule.
  • Edit remote files in your own editor — double-click a file in the SFTP browser and it opens in whatever app you'd normally use, with every save uploaded straight back to the host. It's a private local copy rather than a live mount, but the round trip is invisible: no manual download, edit, re-upload. The browser shows exactly what's checked out and when it last saved, and a protected host asks before the file is opened.
  • Per-host connection options — agent forwarding, X11 forwarding and keepalive, each deferring to ~/.ssh/config until a host sets it. X11 is trusted (-Y) or untrusted (-X) per host, and needs XQuartz (brew install --cask xquartz); hosts with X11 on say so if it's missing or not running.
  • Port forwarding — saved -L / -R / SOCKS tunnels with live status, start/stop, and launch-at-startup, tunneled through any host in the library. Best-effort by design: the status tracks the ssh process, not whether traffic still flows, and nothing restarts a tunnel that dies. See docs/port-forwarding.md — worth two minutes before you depend on one.
  • A welcome screen that searches — the tab bar's + button opens a "welcome aboard" tab with a live host search bar, arrow-key navigation and Enter to launch, focused automatically so you can type right away (picking a host or starting a local shell takes over that same tab); the whole-window empty state and the "jump back in" recent-connections list work the same way. The sidebar's own host filter supports the same arrow-key navigation into its results.
  • Tabs, tuned for a lot of them — right-click a tab to duplicate it (same host(s)/split layout, fresh sessions) or reopen the last one you closed (⇧⌘T); an overflowing tab strip grows </> chevrons to page through it instead of requiring you to know a keyboard shortcut. A dropped session reconnects with a single keystroke (R) from its "session ended" bar.
  • Every shortcut remappable — Settings → Shortcuts lists every keyboard shortcut with a click-to-record rebind, conflict detection, and per-row or one-click reset to defaults. New default shortcuts: Reopen Closed Tab, Toggle MultiExec, Toggle Grid View, Clear Buffer, and the MultiExec pane controls (Toggle Pane ⌥⌘M, Include All ⌥⌘A, Exclude All ⌥⌘E, Invert Selection ⌥⌘I).
  • Cursor styling — block, underline, or bar, with independent blink, in Settings → Appearance with a live preview.
  • Copy/paste from a right-click — a context menu in every terminal, alongside the existing ⌘C/⌘V.
  • Session logging — per-host log folders with compression and search.
  • Keychain passwords — per-host saved passwords supplied to ssh automatically; nothing ever lands in the JSON library. An app-wide default password (Settings → Connection) covers hosts that opt in to saving a password but don't have one of their own yet — pairs with the sidebar's bulk "Save Password in Keychain" action for a multi-selection of hosts, so a big batch of imported hosts sharing one login doesn't need per-host passwords typed in one at a time.
  • Auto-updates — Sparkle-powered in-app updates from GitHub Releases, with the check frequency and on/off switch configurable in Settings → Updates.
  • Host key handling — an optional "automatically accept new host keys" toggle (Settings → Connection) skips the first-connection prompt without weakening protection against a known host's key changing later, which still hard-fails exactly as ssh normally does.

Roadmap

Shipped recently

  • ✅ Configurable scrollback, GPU (Metal) rendering, a tested terminal-compatibility matrix, native sidebar multi-selection with drag-to-folder, session restore, and split panes.

  • ✅ Tab bar polish — keyboard tab switching (⌘⇧[/], ⌘1–9, and a ⌘←/⌘→ alias), pane zoom (⌘⇧↵), reconnect-in-place from the "session ended" bar, a tab context menu (rename / duplicate / close / close others), overflow chevrons, and a configurable MultiExec alert color.

  • ✅ Fully remappable keyboard shortcuts, cursor shape/blink, right-click terminal copy/paste, the searchable welcome/start-page tab, app-wide and bulk password handling, and configurable auto-update checking.

  • ✅ SFTP browser polish — auto-refresh on host switch, delete confirmation, a persistent drag/drop hint, and cd-following with one-click shell integration install + shell auto-detection (bash/zsh). Plus MultiExec arming in one step, a Grid View restore bugfix, working toolbar tooltips, an optional auto-accept-new-host-keys toggle, arrow-key navigation in both the welcome-screen search and the sidebar filter, and single-keystroke session reconnect.

  • ✅ Named credential profiles — multiple reusable identities (username + SSH key and/or password), managed in Settings ▸ Profiles and applied in bulk to a multi-selection or a folder, so a fleet split across a handful of shared accounts (AD, Ansible/Nutanix service accounts, IPMI/vendor consoles, etc.) doesn't need per-host credential entry. A host holds a live reference to its assigned profile rather than a one-time copy, so rotating a profile's password or key updates every host using it immediately. The old single default password folds into this as the first profile ("Default").

  • ✅ Pinned favorites on the welcome/start page, shown alongside "Jump back in" — hidden while actively searching, same as recents. Pin from a sidebar right-click (single host or a multi-selection), a hover star icon on each sidebar row, or a Favorite toggle in the session editor.

  • ✅ App appearance (light / dark / follow system), independent of the terminal's color theme; favourite macros pinned to the MultiExec bar; browsable recently-closed tabs; ⌘W closes the tab; a "never connected" coverage category; and inline images documented at last — Sixel, iTerm2 and Kitty graphics all render.

  • ✅ Host-to-host file copy — drag a file from the SFTP browser onto a different pane and it lands on that host, in whatever directory that pane's shell is currently sitting in. Drop it on a pane that is broadcasting and it goes to every host in the MultiExec group at once; files dragged in from Finder follow the same rule. The bytes relay through a staging file on the Mac (there is no safe direct host-to-host path without trusting one box with credentials for another), downloaded once no matter how many hosts receive it, and uploaded to several at a time — configurable in Settings ▸ Connection.

  • ✅ Changelog on update — the Sparkle update prompt now shows a cumulative changelog covering everything since whatever version you're updating from, not just the latest release's own notes, since releases ship fast enough that auto-updaters often jump several versions at once.

  • ✅ Agent Access — Claude Code, Codex or your own scripts can list hosts, open sessions, and (with a second switch) type into and read them, through the portside command or as MCP tools (portside mcp). Every program is approved by name; protected hosts and multi-line input ask every time; commands are staged for you to run unless the agent is told otherwise; an agent never arms MultiExec or types at a password prompt. Optional, scoped "Don't Ask" mode for trusted setups. See docs/agent-api.md.

  • ✅ Shared inventory — subscribe to a team's hosts, and the containers on them, in a git repo, shown read-only beside your own, with your own settings layered on top. Plain git, fast-forward only, manifest sanitized. See docs/shared-inventory.md.

Next up

  • Nothing named. See Later for the standing list.

Later

  • Per-profile font/theme choices (appearance is global today).
  • Font ligatures are a current SwiftTerm limitation, tracked in the compatibility matrix rather than promised here. (Shell integration / prompt markers were previously listed here as blocked upstream. They aren't: Portside parses OSC 133 itself from the raw byte stream, which is how command history works — SwiftTerm never needs to see them.)
  • Touch ID gating for saved credentials (Vaultwarden references later)
  • Named / pinned layout presets
  • tmux control-mode (-CC) integration — native splits backed by a durable remote tmux session

Building

Requires the Swift 6 toolchain (Xcode Command Line Tools are enough). The package itself is still on swift-tools-version: 5.9 and builds in Swift 5 language mode — the Swift 6 concurrency migration hasn't happened yet. CI holds the line with a per-file ratchet (Scripts/strict-concurrency-check.sh) so the outstanding warning count can shrink but never grow:

# Run for development
swift run

# Run against a throwaway library instead of your real one
PORTSIDE_LIBRARY_DIR=/tmp/portside-dev swift run

# Build a standalone Portside.app
Scripts/make_app.sh
open build/Portside.app

swift run otherwise shares everything with the installed app — the same hosts, the same saved workspace, the same history — so a dev build is a small risk to real data every time, and each launch stops to ask whether to restore the session you left open in the other copy. PORTSIDE_LIBRARY_DIR points it somewhere disposable instead; seeding from ~/.ssh/config is off in that mode, so the library starts genuinely empty and your real hostnames stay out of any screenshots.

A swift run build also has no Info.plist, so it has no Sparkle feed and deliberately doesn't start the updater — no "couldn't check for updates" dialog on launch, and "Check for Updates…" disables itself. Packaged builds are unaffected: Scripts/make_app.sh writes the feed URL and public key.

Status & contributions

Pre-1.0 and evolving quickly; expect sharp edges. Issues and ideas welcome — especially from fellow operators who live in the terminal.

About

A fast, beautiful, native macOS workbench for people who live on SSH.

Topics

Resources

Contributing

Security policy

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages