All posts
Engineering July 5, 2026 3 min Bastion contributors

Why we built a local-first SSH client

The case for keeping hosts, keys, and settings on the device, and the trade-offs that come with that.

ssh local-first architecture

Every SSH client we tried put our fleet somewhere we couldn’t see. The host list synced to a vendor’s cloud. The keys were sometimes stored in their backend. The settings, the snippets, and the scrollback all eventually lived on someone else’s machine.

That’s the kind of convenience that erodes trust. And SSH is the protocol that guards the keys to the kingdom. We didn’t want to keep building on top of software that quietly assumed we were fine with that.

So we built Bastion Terminal as a local-first product. Here’s what that means in practice, and why it was worth the trade-offs.

What local-first means here

A small definition: local-first means the source of truth for your data lives on your device. Sync, if it happens at all, is a derived copy, not the authoritative one.

For Bastion, the source-of-truth layers are:

DataStorage
Hosts, groups, snippets, settingsLocal SQLite (bastion.db)
Private keysOS keychain
PasswordsMemory only (active connect)
Terminal scrollbackIn-memory

Nothing in that list is on our servers, because we don’t run any. There’s no account to create, no API token to mint, no dashboard to lose access to.

The trade-offs we’d accept

Building local-first wasn’t free. We gave up a few things:

  • Multi-device sync. Version 1.1.0 does not sync hosts or keys to another device. A future sync design is not confirmed.
  • “Just sign in” onboarding. First-run is a window, not an account. People who want zero friction from the App Store to a working SSH session are not our audience.
  • A telemetry client. No sender or crash reporter was found in the desktop source. A local telemetry flag exists and the settings save path forces it off. That is a source review, not an audit.

We accepted those costs because the alternative, a vendor cloud holding a list of every production server you SSH into, is exactly the threat model SSH exists to push back against.

The trade-offs we won’t accept

Some things can’t be traded away, even for local-first:

  • Passwords on disk. We don’t persist them. They’re held in memory for the active connect and dropped on disconnect. (Key auth doesn’t need this in the first place.)
  • Silent host-key rollover. Every connect compares the server-presented fingerprint against the trusted entry. A mismatch is a hard block.
  • A public update channel. Version 1.1.0 checks latest.json on downloads.bastion.hexifytech.com, and SHA256SUMS.txt is on that host.
  • Silent AI uploads. Asking a question sends the attached context to the provider you configure. Version 1.1.0 builds a redacted context pack first. Proposed commands still wait for approval. Redaction has not been independently audited.

These are not features. They’re constraints that shape every other piece of the workspace.

What we kept

Local-first doesn’t have to feel old. The product is still a polished desktop app on Tauri 2, with tabs, splits, a host manager, snippets, a command palette, and themes. The terminal renders with xterm.js. The workspace feels modern; the storage model is just quiet about itself.

Where to go from here

If you’re evaluating Bastion:

  1. Download Linux or Windows 1.1.0 from the download page.
  2. Read the security model to see exactly what we store and where.
  3. If you want to dig into the source, the architecture overview explains the modules.

We’re not going to convince everyone that local-first is right. But for the engineers who feel the same way we do, we wanted to build a tool that doesn’t ask you to compromise on it.