This is the OS-neutral contract for BHTerminal. The macOS app
(../macos/) is the reference implementation; the Windows and
Linux ports are native apps that provide the same features and stay
config-compatible with it.
Unlike a keyboard engine, a terminal client has no single portable core — SSH, SFTP, VNC, PTY, credential storage, and UI all have different best-in-class libraries per OS. So the shared thing here is not code, it's this spec: the feature set, the data model, and the security rules every port must honor.
A MobaXterm/Termius-style workspace in one window:
- A session tree (folders + hosts) on the left.
- A terminal (tabs + split panes) on the right.
- An SFTP file browser that follows the terminal's working directory.
- VNC remote-desktop tabs in the same window.
- SSH tunnels (local / remote / dynamic) that keep running in the background.
- Import from MobaXterm and OpenSSH config.
All ports read/write the same logical model so a user's sessions are portable.
Host
| field | type | notes |
|---|---|---|
id |
UUID | stable identity; also the Keychain/credential key |
name |
string | display name |
hostname |
string | must pass §5 validation |
port |
int | 22 (ssh) / 5900 (vnc) default |
username |
string | must pass §5 validation |
authMethod |
password | privateKey(path) | agent |
|
connectionType |
ssh | vnc |
|
jumpHostID |
UUID? | ProxyJump |
folderID |
UUID? | tree placement |
tunnels |
[TunnelRule] |
|
vncSharedClipboard |
bool | default false |
Secrets (password, passphrase) are never in this model — they live in the
OS credential store keyed by host.<id>. The model persists as JSON
(sessions.json on macOS). Keep the field names identical so exported configs
move between platforms.
| Subsystem | macOS (reference) | Windows | Linux |
|---|---|---|---|
| Terminal emulator view | SwiftTerm | [xterm.js] (Electron) / ConPTY + a native widget / WinUI | VTE (GTK) / xterm.js (Electron) |
| Interactive SSH | system /usr/bin/ssh in a pty |
ssh.exe (OpenSSH for Windows) via ConPTY, or [libssh2] |
system ssh in a pty |
| SFTP (structured) | the SFTP subsystem over real ssh (ssh -s … sftp), reusing the terminal's multiplexed connection |
same (ConPTY ssh.exe -s) |
same (ssh -s) |
| VNC | RoyalVNCKit | [noVNC]/native RFB | [gtk-vnc]/noVNC |
| Credential store | Keychain | Windows Credential Manager (DPAPI) | Secret Service / libsecret (GNOME Keyring / KWallet) |
| Tunnels | background ssh -N -L/-R/-D Process |
same, with ssh.exe |
same |
| known_hosts verify | ~/.ssh/known_hosts |
%USERPROFILE%\.ssh\known_hosts |
~/.ssh/known_hosts |
| Launch at login | SMAppService |
Startup registry / Task Scheduler | .desktop autostart |
| Menu-bar / tray | MenuBarExtra |
system tray (NotifyIcon) | StatusNotifierItem (AppIndicator) |
Recommended reuse of the terminal: just like macOS, spawn the platform's own
ssh binary in a pty (ConPTY on Windows, a pty on Linux). You inherit the user's
SSH config, agent, keys, ProxyJump, and 2FA for free, and you don't reimplement
crypto. Do the same for SFTP: open the SFTP subsystem over real ssh
(ssh -o ControlPath=<shared> -s -- host sftp) reusing the terminal's
multiplexed master connection, and speak the SFTP v3 wire protocol over that
process's stdin/stdout (see macos/BHTerminal/SSH/SFTPProtocolClient.swift).
That way the file browser gets keys/agent/passphrase/2FA/ProxyJump for free too —
no in-process SSH library, no separate credential path.
- Double-clicking a host connects the terminal and the SFTP pane together.
- The SFTP breadcrumb follows the terminal's cwd (emit/parse OSC 7).
- Terminal tabs + split panes; broadcast-input to all panes in a tab; snippets.
- Closing the window keeps the app running in the tray/menu-bar; tunnels survive.
- Import: MobaXterm passwords
.txt(host+port+user+password → credential store), MobaXterm.mxtsessions(names+folders, no passwords), and~/.ssh/config. See../macos/README.md.
These are not optional; they're why BHTerminal is safe to hand your servers to.
- Secrets only in the OS credential store, never in the JSON config or logs.
- Host-key verification. Running both terminal and SFTP over the real
sshbinary gets you the user'sknown_hostshandling for free — keep it that way. If a port ever uses an in-process SSH library instead, it must verify againstknown_hostsand fail closed (never trust-on-first-use). - Argument-injection guard. Before building any
sshargv, validatehostnameandusernameagainst an allow-list (letters, digits, and.-_:%[]for hosts;.-_for users; no leading-, no whitespace/shell metacharacters) and reject the rest. End option parsing with--before the destination operand. A host named-oProxyCommand=…must never reachssh. - Imports are untrusted. Re-validate every hostname/username at import; clamp/strip imported names; size-cap the import file.
- The remote SFTP server is untrusted. If you parse the SFTP wire protocol, treat every byte from the server as hostile: bound server-declared packet lengths and array counts (a NAME reply can claim 4 billion entries), cap in-memory buffers / stream large files to disk, and put a timeout on every request so a stalled server can't hang the app.
- SSH control socket in a per-user 0700 directory, never a world-writable
path like
/tmp— the%Cname is predictable and a shared-dir socket can be squatted to MITM the multiplexed session. - VNC clipboard sharing defaults off (opt-in per host).
- Ship with the platform's exploit-mitigation defaults (Hardened Runtime on macOS; equivalent on others).
- Read this file end-to-end.
- Build the headless core first: the data model (§2), the credential-store
wrapper, the argv builder + validator (§5.3), and the import parsers (§4) —
with unit tests, no UI. The macOS versions in
../macos/BHTerminal/(Models/,Persistence/,Utilities/SSHSafety.swift,SSH/KnownHostsValidator.swift,Import/) are the reference to translate. - Wire the terminal (spawn
sshin a pty), then the SFTP pane, then VNC, then tunnels, then the tray + launch-at-login. - Verify against a real host — this is an app whose whole value is "does it actually SSH/SFTP/VNC correctly."
Framework choice is yours (native WinUI/GTK/Qt for the smallest, best-feeling app; or Electron+xterm.js+ssh2 for the fastest shared-UI route). Keep the core a clean, testable module regardless.