Skip to main content
Glama
konraddzbik

termdesk

by konraddzbik

TermDesk

A cross-platform SSH + SFTP + VNC + RDP desktop client โ€” multi-tab terminals, streaming file transfers and remote desktops (VNC over SSH, plus native RDP), in one window.

Electron React TypeScript CI License

Status: shipping โ€” SSH terminal, SFTP, VNC over SSH, native RDP, MCP agent access, SSH tunnel manager, fleet automation, local terminals with a choice of terminal program (login shell / tmux / Zellij / screen / alternate shells), Prompt Book + scheduled Routines, settings, command palette and packaging.


โœจ Features

  • ๐Ÿ–ฅ๏ธ Flexible hosts โ€” create SSH-only, VNC-only, or combined hosts at creation time. SSH and VNC are independently optional. Pure VNC hosts support direct TCP or SSH-tunneled connections (with clear error if you try to tunnel without SSH creds). UI actions, command palette, and test buttons adapt per host kind.

  • ๐Ÿ–ฅ๏ธ Multi-tab SSH terminal โ€” xterm.js (WebGL) with search, copy/paste, resize; many parallel sessions to the same or different hosts. Early output is buffered until the terminal attaches, so MOTD/banners aren't lost.

  • ๐Ÿ“ SFTP with a streaming transfer queue โ€” remote file browser, drag & drop (recursive folder upload), chunk-by-chunk streaming with constant memory, cancel/retry, edit-in-place with auto-upload on save.

  • ๐Ÿ”’ VNC over SSH โ€” noVNC rendered in-app, fed through a loopback WebSocket bridge guarded by single-use, 30 s tokens; default transport is an SSH forwardOut tunnel, so port 5900 is never exposed. Direct mode also supported for VNC-only hosts.

  • ๐Ÿ–ฅ๏ธ RDP โ€” connect to Windows/RDP hosts in-app via an IronRDP WASM client fed through an in-process RDCleanPath proxy (same single-use, 30 s-token, origin-checked, TLS-terminating shape as the VNC bridge), with trust-on-first-use server-certificate pinning.

  • ๐Ÿ” Encrypted vault โ€” hosts, groups, snippets and known hosts in SQLite; passwords/passphrases encrypted with OS-keychain-backed safeStorage the moment they arrive in the main process.

  • ๐Ÿ›ก๏ธ Host-key verification โ€” SHA256 fingerprint approval dialog on first connect, hard block on a same-type key change, and a distinct loud "possible man-in-the-middle" warning when a known host presents a key/type never trusted for it.

  • ๐Ÿชœ ProxyJump โ€” multi-hop chains (user@jump:port,next) via chained forwardOut.

  • ๐Ÿ”Œ SSH tunnel manager โ€” define, persist and start/stop local (-L) port forwards and dynamic SOCKS5 (-D) proxies from a sidebar panel, with a live status dot + throughput. Reuses an open terminal's connection when possible; destructive-free, owner-scoped, logged. See docs/TUNNELS.md.

  • ๐ŸชŸ Split panes โ€” view two sessions side by side or stacked (Alt-click a tab or the split toolbar), with a draggable divider. Great paired with multi-host automation for a live monitoring grid.

  • ๐Ÿ’ป Local terminals โ€” node-pty shell tabs on your own machine, with saved working directories โ€” no SSH required. Per-host default remote path is honored on connect.

  • ๐Ÿงฉ Choose your terminal program โ€” pick what runs when a terminal opens (Settings โ†’ General): your default login shell, a multiplexer (tmux, Zellij, GNU Screen โ€” persist across restarts/disconnects) or an alternate shell (bash, zsh, fish, PowerShell, Nushell). Only programs detected on your machine are selectable; the choice applies to both local terminals and SSH sessions (exec'd on the remote when present, else a plain shell).

  • ๐Ÿš€ Open in external terminal โ€” hand the current directory off to your favorite GUI terminal โ€” Ghostty, Warp, iTerm2, kitty, Alacritty, WezTerm, GNOME Terminal, Konsole, Windows Terminal โ€” detected on your machine, with a saved default (Settings โ†’ General), from โŒ˜K or a one-click button on any local terminal tab.

  • โšก Fleet automation โ€” run a snippet or command across a whole group of SSH hosts at once, with live per-host streaming output.

  • ๐Ÿ•˜ Activity log โ€” a local timeline of connects/disconnects, SFTP/VNC opens and automation runs. Metadata only: secret-bearing command tokens are redacted and entries purge after 90 days.

  • ๐ŸŽจ Terminal color schemes โ€” built-in Dracula, Solarized Dark, Gruvbox, One Dark, Nord (plus the default), per your preference.

  • ๐Ÿงฑ Customizable sidebar โ€” show or hide each left-sidebar section (Hosts, Local terminals, Workspaces, Tunnels, Snippets, Prompt Book, Routines) from Settings โ†’ General or the sidebar's Customize button. Your choice persists; everything is visible by default.

  • ๐Ÿงฉ Prompt Book โ€” reusable, templated prompts for AI agents. Prompts are plain text with {{variable}} placeholders (optionally {{name:default}} / {{name|description}}); running one asks for the values, shows a live preview, and either sends the rendered text to the active terminal (SSH or local) or launches it in an AI agent โ€” Claude Code, Aider, OpenCode, Codex or Gemini โ€” in a directory you pick. Templating is pure substitution (no eval) and the prompt is passed as a single quoted argument, so shell metacharacters in it are inert.

  • ๐Ÿ” Routines โ€” saved "run this prompt through this agent in this directory" jobs, run on demand or on a schedule (interval / daily / cron), each with its own run history. Routines fire while TermDesk is open, and a run missed while the app was closed catches up once on next launch instead of stampeding every missed slot. Unattended autonomy is opt-in and off by default; stored run summaries are secret-redacted.

  • ๐Ÿ“‹ Snippets โ€” saved commands sent to the active session.

  • โŒจ๏ธ Command palette โ€” โŒ˜/Ctrl+K fuzzy host search + every command (terminals, SFTP, VNC, automation, logs); press ? for the shortcuts cheat-sheet.

  • โš™๏ธ Settings โ€” theme (dark / light / System, following the OS), terminal font + color scheme, right-click-paste behavior, SSH keepalive; plain JSON, never secrets.

  • ๐Ÿค– AI agent access (MCP) โ€” let Claude, Cursor, Grok or any MCP client use TermDesk: list hosts, run commands, fan out across a fleet. The agent gets hands, never keys โ€” credentials stay in the main process, every action is per-host opt-in + approval-gated + shown live in an AI Activity log. Off by default. See docs/MCP-INTEGRATION.md.

  • ๐Ÿ  Local-first โ€” your hosts, keys and history never leave your machine; no mandatory cloud account, no telemetry.

Related MCP server: SentryFrogg MCP Server

๐Ÿ— Architecture

All SSH/SFTP/VNC logic lives in the main process. The renderer is fully sandboxed (contextIsolation: true, nodeIntegration: false, sandbox: true) and talks to it only through a typed, Zod-validated IPC contract (src/shared/ipc.ts).

flowchart LR
    subgraph R["Renderer (sandboxed, no Node)"]
        UI["React 19 UI<br/>zustand stores"]
        XT["xterm.js<br/>terminal"]
        NV["noVNC<br/>RFB client"]
    end

    subgraph M["Main process"]
        IPC["Typed IPC<br/>Zod-validated contracts"]
        SM["SessionManager<br/>(ssh2: shell, agent,<br/>keys, ProxyJump)"]
        SF["SftpManager +<br/>TransferManager<br/>(streaming queue)"]
        WB["ws-bridge<br/>127.0.0.1, random port,<br/>single-use 30s tokens"]
        VA[("Vault<br/>better-sqlite3 + Drizzle<br/>secrets via safeStorage")]
    end

    RH[("Remote host<br/>sshd ยท sftp ยท vncserver")]

    UI <-->|"invoke/handle"| IPC
    XT <-->|"ssh:data:#lt;sessionId#gt; stream"| IPC
    IPC <--> SM
    IPC <--> SF
    SM --- VA
    SF --- SM
    NV -->|"ws://127.0.0.1:#lt;port#gt;/#lt;token#gt;"| WB
    WB -->|"variant B (default):<br/>ssh2 forwardOut โ†’ :5900"| SM
    WB -.->|"variant A (opt-in):<br/>direct TCP โ†’ :5900"| RH
    SM <-->|"SSH"| RH

Two deliberate exceptions to "secrets stay in main": the stored VNC password (for the RFB credentials handshake) and the stored RDP password (for the IronRDP client) are decrypted in main and returned to the renderer. Neither is currently bound to an open tab or rate-limited โ€” see Known limitations.

๐Ÿš€ Quick start

TermDesk is currently run from source. konraddzbik/termdesk has no tags and no GitHub Releases, so there is nothing to download from the Releases page yet โ€” see INSTALL.md.

You need Node.js >=22.12.0 and npm 10+, plus a C/C++ toolchain (Xcode Command Line Tools / build-essential / Visual Studio C++ build tools) so the native modules can compile. On Linux you also need an unlocked OS keyring (gnome-libsecret or kwallet) โ€” TermDesk fails closed rather than use Electron's insecure basic_text fallback, so without one you cannot save a host password (SECURITY.md).

npm install        # also applies the better-sqlite3 patch + rebuilds native deps
npm run dev        # electron-vite dev server + Electron window, HMR

First run: npm install && npm run dev opens a usable app โ€” there is no licence check, no seat and no account. See CONTRIBUTING.md for the full development setup, including the better-sqlite3 ABI step you need before npm test will run.

Script

What it does

npm run dev

electron-vite dev server + Electron window

npm run doctor

preflight: Node, toolchain, better-sqlite3 ABI, Linux keyring

npm run build

typecheck (TS strict) + production build to out/

npm run lint / lint:fix

biome check (format + lint)

npm test

vitest (unit + integration)

npm run test:coverage

vitest with v8 coverage

npm run dist

build + electron-builder (dmg / NSIS / AppImage)

npm run dist produces an unsigned installer for the host OS (dist/TermDesk-<version>-arm64.dmg on Apple Silicon macOS, plus a cross-built x64 .dmg). It does not publish anything. See INSTALL.md for from-source setup, local artifact names, and how a future GitHub Release would be cut.

๐Ÿงช Dev test environment

A single Docker container provides SSH + TigerVNC for local testing โ€” no real servers needed:

npm run test:keys                                  # throwaway SSH keys (untracked)
docker compose -f docker-compose.test.yml up -d

Service

Endpoint

Credentials

SSH

127.0.0.1:2222

testuser / testpass123, or the generated keys in .test/ (test_key, test_key_enc)

VNC

127.0.0.1:5901 (direct or via the SSH tunnel โ€” same container)

testvncpass

E2E smoke harnesses

Five self-contained end-to-end suites run inside the real Electron app (the first four against the docker container; the MCP suite needs no external services). Each prints an *_OK marker on success:

Command

Proves

Verified result

TERMDESK_SMOKE=vault npx electron .

Secrets are safeStorage-encrypted before persisting; no plaintext in SQLite; never returned to the renderer

VAULT_SMOKE_OK

TERMDESK_SMOKE=ssh npx electron .

Real logins with password, key, and key + encrypted passphrase

SSH_SMOKE_OK

TERMDESK_SMOKE=sftp npx electron .

1 GB upload + download with RSS monitoring (peak 259 MB โ€” under the 300 MB budget) and a 500-file folder upload

SFTP_SMOKE_OK

TERMDESK_SMOKE=vnc npx electron .

RFB handshake both direct and over the SSH tunnel; forged-token connections rejected; tokens are single-use

VNC_SMOKE_OK

TERMDESK_SMOKE=mcp npx electron .

Token-gated MCP server starts on loopback; a forged bearer token is rejected

MCP_SMOKE_OK

โœ… Testing

npm test                # unit + integration suite (vitest) โ€” all green on main
npm run test:coverage   # v8 line coverage report into coverage/

Vitest runs renderer tests under jsdom (Testing Library) and everything else under node. Coverage is concentrated where it counts โ€” pure logic is high, while process-glue and UI shells are covered by the five e2e smokes instead of unit tests (percentages below are indicative, not pinned โ€” run npm run test:coverage for current numbers):

  • High (lines): shared 97.1% โ€” the Zod IPC contract every process depends on โ€” renderer/lib 97.5%, main/store 81.4% (db, hosts-repo, settings and snippets-repo at or near 100%), ssh-util 100%, the ~/.ssh/config parser and its Include resolver in the high 90s.

  • Low, and covered by the smokes instead: main/ipc handler glue 9.8%, session-manager and sftp-manager/transfer-manager 0%, vnc-manager and the UI shells (SftpTab, TerminalView, VncTab, layout and the sidebar panels) low or zero. These are process glue and Electron/DOM shells; the TERMDESK_SMOKE harnesses exercise them end to end against a real server. Entry points and *-smoke.ts files are excluded from coverage by config.

Build + typecheck + lint are clean, and a headed dev launch logs zero error lines.

โŒจ๏ธ Keyboard shortcuts

Shortcut

Action

โŒ˜/Ctrl+K

Command palette (fuzzy host search + commands)

โŒ˜/Ctrl+T

New session (opens the palette)

โŒ˜/Ctrl+W

Close active tab

โŒ˜/Ctrl+F

Search in terminal

โŒ˜/Ctrl+Shift+C/V

Copy/paste in terminal

โš™๏ธ Settings

Theme (dark, light, or System โ€” following the OS prefers-color-scheme), terminal font size/family, SSH keepalive interval โ€” stored as plain JSON in userData/settings.json (never secrets).

๐Ÿ“ก Feature details

SSH terminal

  • All ssh2 logic lives in the main process (src/main/ssh/session-manager.ts); the renderer only sees a typed stream API. Output streams on ssh:data:<sessionId>; early output is buffered until the terminal attaches.

  • Auth: vault password (with keyboard-interactive fallback), private key (+ encrypted passphrase), or SSH agent (SSH_AUTH_SOCK, Windows OpenSSH pipe). Secrets are decrypted only at connect time.

  • Host keys: first connect shows a SHA256 fingerprint approval dialog and persists to the known_hosts table; a mismatch hard-blocks the connection.

  • ProxyJump chains (user@jump:port,next) via chained forwardOut; keepalive 15 s.

SFTP

  • SFTP sessions reuse the live SSH connection of an open terminal to the same host (no second login) and fall back to a dedicated shell-less connection.

  • Transfers stream chunk-by-chunk (constant memory โ€” 1 GB verified under 300 MB RSS) and are cancellable/retryable; folder drops upload recursively with structure preserved.

  • Edit-in-place downloads to a temp file, opens the OS editor and auto-uploads on save.

VNC

  • noVNC renders in the renderer; it speaks WebSocket to a main-process bridge bound to 127.0.0.1 on a random port. Every connection requires a single-use, 30 s token โ€” other local processes can't ride the bridge (hence ws://127.0.0.1:* in the CSP).

  • Default transport is an SSH forwardOut channel (variant B โ€” port 5900 never exposed), reusing a live terminal connection to the host when one exists; plain TCP (variant A) is opt-in per host.

  • The stored VNC and RDP passwords are decrypted in main and returned to the renderer for their respective clients โ€” the two documented exceptions to "secrets stay in main".

  • Toolbar: local scaling / remote resize toggles, clipboard paste, Ctrl+Alt+Del, fullscreen, reconnect (each reconnect provisions a fresh token + tunnel).

Vault

  • Hosts, groups, snippets and known_hosts live in SQLite (better-sqlite3 + Drizzle) under the app's userData directory.

  • Passwords/passphrases are encrypted with safeStorage (OS keychain key) in the main process the moment they arrive; only ciphertext blobs touch the database, and secrets are never sent back to the renderer.

  • ~/.ssh/config import parses Host/HostName/Port/User/IdentityFile/ProxyJump, resolves Include directives (tilde/relative/absolute paths and globs, with cycle/depth/size guards) and merges Host */wildcard defaults into concrete hosts using OpenSSH first-obtained-wins ordering.

๐Ÿ” Security checklist

  • contextIsolation: true, nodeIntegration: false, sandbox: true โ€” the renderer and preload never touch Node.

  • Strict CSP via meta tag: script-src 'self', no unsafe-eval, no remote module; connect-src allows only 'self' and loopback WebSockets (the token-gated VNC bridge). Dev-only relaxation happens through a Vite plugin that fails loudly on drift.

  • webSecurity: true; navigation origin-checked; window.open denied (external https: โ†’ system browser); <webview> blocked; permission requests denied by default.

  • Every IPC payload is validated with Zod (src/shared/ipc.ts is the single contract); invoke results are typed end-to-end via IpcInvokeMap.

  • Secrets (SSH/VNC passwords, key passphrases) are encrypted with safeStorage in the main process the moment they arrive; only ciphertext is persisted; decryption happens at connect time and references are dropped immediately. Verified by TERMDESK_SMOKE=vault. Two documented exceptions: the VNC and RDP passwords cross to the renderer for their protocol clients (see Known limitations).

  • Errors crossing IPC are sanitized (first line only, no stacks/paths); secrets are never logged.

  • Sessions, SFTP handles, transfers and host-key prompts are owner-scoped to the WebContents that created them and torn down when it is destroyed.

  • Host keys: SHA256 fingerprint approval on first connect, hard block on mismatch โ€” for the final target and every ProxyJump hop.

  • Activity log is metadata-only: OS username, secret-bearing command tokens redacted, 90-day purge.

  • Optional master password (Argon2id-derived second encryption layer) โ€” deferred, tracked as a future enhancement.

See SECURITY.md for the full security model, the known limitations, and the vulnerability-disclosure policy.

๐Ÿ“‚ Project structure

src/
  main/                  # Electron main process โ€” all SSH/SFTP/VNC logic lives here
    ipc/                 # IPC handlers per domain (ssh, sftp, vnc, hosts, โ€ฆ)
    ssh/                 # session-manager, ssh-config-parser, ssh-util (+ smoke)
    sftp/                # sftp-manager, transfer-manager, edit-watch (+ smoke)
    vnc/                 # ws-bridge, vnc-manager (+ smoke)
    store/               # Drizzle + better-sqlite3, secrets (safeStorage), settings
  preload/               # contextBridge โ€” typed, minimal API surface (window.api)
  renderer/              # React UI (no Node access)
    components/{layout,hosts,terminal,sftp,vnc,snippets,ui}/
    hooks/  stores/  lib/
  shared/                # IPC channel contracts + Zod schemas shared by all processes

๐Ÿ—บ Known limitations / roadmap

  • Master password โ€” optional Argon2id-derived second encryption layer over the vault; deferred.

  • Playwright smoke test (launch app โ†’ add host โ†’ mock connection) โ€” deferred; the five TERMDESK_SMOKE harnesses cover e2e today.

  • ~/.ssh/config import โ€” Match blocks and token expansion (%h/%p) are still skipped; cloud-provider inventory import (AWS/GCP) is not implemented.

See SECURITY.md for the hardening roadmap.

๐Ÿ”ง Native-module notes

  • better-sqlite3 needs a source patch to compile against Electron's V8 (no upstream prebuilds yet) โ€” applied automatically via patch-package on npm install (patches/better-sqlite3+12.10.0.patch).

  • cpu-features (optional ssh2 native dep) is overridden to noop2 in package.json to avoid an unnecessary native build; ssh2 falls back to its pure-JS path.

๐Ÿค Contributing

Issues and pull requests are welcome. The delivery plan for outsider install, packages, and contributor setup is docs/OSS-DELIVERY-PLAN.md. CONTRIBUTING.md has the development setup โ€” including the two native-module traps that will otherwise cost you an afternoon โ€” the invariants new code has to keep, and the pre-PR checklist. Contributions are inbound=outbound MIT: no CLA, no copyright assignment.

Participation is governed by the Code of Conduct. Security problems go through the private channel in SECURITY.md, never a public issue.

๐Ÿ“„ License

The source in this repository is released under the MIT License โ€” see LICENSE. You may use, modify, and redistribute it, including commercially, as long as the copyright notice and licence text travel with it.

When prebuilt installers exist on the Releases page they are additionally covered by EULA.txt, which governs those binaries. There are currently no GitHub Releases, so a build you make yourself from this source is governed by LICENSE alone.

There is no licence check, seat activation or account in this client: the commercial licensing subsystem was removed when the source was published, so what you build from this repository is the whole application.

That boundary is enforced in code, not just described here: the first-run EULA prompt is compiled into the project's own release builds only, so a fork's npm run dist, a distro package or your own installer never shows it.

Third-party dependencies keep their own licences, listed in THIRD-PARTY-NOTICES.md. One carries an obligation worth naming: @novnc/novnc is MPL-2.0 and this repository patches it (patches/@novnc+novnc+1.7.0.patch), so that modification is published here under MPL-2.0 along with the rest of the source.

Maintenance

ActivityMaintained
ResponsivenessResponsive

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables SSH interactive session management through MCP, supporting commands, menus, and session lifecycle operations.
    1
  • A
    license
    Not graded
    quality
    B
    maintenance
    MCP server that provides SSH tools (read-only probes and arbitrary exec) to a fleet of hosts outside Kubernetes, with an inventory-based allowlist and key-based authentication.
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/konraddzbik/termdesk'

If you have feedback or need assistance with the MCP directory API, please join our Discord server