Self-hosted SSH + SFTP + RDP in a browser — MobaXterm replacement
Go to file
Vantz Stockwell de0fd0556c
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 1m7s
fix: font measurement race + updater download URL mismatch
Two fixes:

1. Terminal font rendering: xterm.js was calling fitAddon.fit() before
   fonts loaded. canvas.measureText() used a fallback font, got wrong
   cell dimensions (2-3px per char instead of 8-9px), producing 200+
   column terminals where text appeared as tiny dashes with colored
   blocks. Fixed by waiting for document.fonts.ready before fitting.
   Also prioritized Windows-native fonts (Cascadia Mono, Consolas)
   in the font stack.

2. Updater download URL: tagVersion used raw release.TagName ("v0.8.3")
   but CI uploads packages under stripped version ("0.8.3"). Download
   URL was .../v0.8.3/wraith-v0.8.3-setup.exe but the actual package
   is at .../0.8.3/wraith-0.8.3-setup.exe. Now uses latestVer (stripped).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 14:22:53 -04:00
.gitea/workflows fix: CI release tag_name must include v prefix to match git tag 2026-03-17 13:56:21 -04:00
docs docs: fired XO audit — spec vs reality gap analysis 2026-03-17 13:26:03 -04:00
frontend fix: font measurement race + updater download URL mismatch 2026-03-17 14:22:53 -04:00
images fix: remove shell integration injection — echoes visibly in terminal 2026-03-17 13:50:10 -04:00
internal fix: font measurement race + updater download URL mismatch 2026-03-17 14:22:53 -04:00
.gitignore Wraith v0.1.0 — Desktop SSH + RDP + SFTP Client 2026-03-17 08:19:29 -04:00
go.mod Wraith v0.1.0 — Desktop SSH + RDP + SFTP Client 2026-03-17 08:19:29 -04:00
go.sum Wraith v0.1.0 — Desktop SSH + RDP + SFTP Client 2026-03-17 08:19:29 -04:00
LICENSE Wraith v0.1.0 — Desktop SSH + RDP + SFTP Client 2026-03-17 08:19:29 -04:00
main.go refactor: remove Claude AI copilot panel — will be replaced with embedded terminal 2026-03-17 12:58:31 -04:00
README.md Wraith v0.1.0 — Desktop SSH + RDP + SFTP Client 2026-03-17 08:19:29 -04:00

Wraith

Wraith

Native desktop SSH + RDP + SFTP client — a MobaXTerm replacement built with Go and Vue.

Go Wails v3 Vue 3 License


Features

  • Multi-tabbed SSH terminal with xterm.js + WebGL rendering
  • SFTP sidebar on every SSH session (MobaXTerm's killer feature) -- same SSH connection, separate channel
  • RDP via FreeRDP3 dynamic linking (purego, no CGO)
  • Encrypted vault -- master password derived with Argon2id, secrets sealed with AES-256-GCM
  • Connection manager with hierarchical groups, tags, color labels, and full-text search
  • 7 built-in terminal themes -- Dracula, Nord, Monokai, One Dark, Solarized Dark, Gruvbox Dark, MobaXTerm Classic
  • Tab detach / reattach -- sessions live in the Go backend; tabs can be torn off into separate windows and reattached without dropping the connection
  • MobaXTerm import -- plugin interface for .mobaconf and other formats
  • Command palette (Ctrl+K) for quick connection search and actions
  • Single binary -- ships as wraith.exe + freerdp3.dll, no Docker, no database server

Tech Stack

Backend (Go)

Component Technology Purpose
Framework Wails v3 Desktop shell, multi-window, type-safe Go-to-JS bindings
SSH golang.org/x/crypto/ssh SSH client, PTY, key/password auth
SFTP github.com/pkg/sftp Remote filesystem over SSH channel
RDP FreeRDP3 via purego RDP protocol, bitmap rendering
Database SQLite via modernc.org/sqlite (pure Go) Connections, credentials, settings, themes
Encryption AES-256-GCM + Argon2id Vault encryption at rest

Frontend (Vue 3 in WebView2)

Component Technology Purpose
Framework Vue 3 (Composition API) UI framework
Terminal xterm.js 5.x + WebGL addon SSH terminal emulator
CSS Tailwind CSS 4 Utility-first styling
Components Naive UI Tree, tabs, modals, dialogs
State Pinia Reactive stores for sessions, connections, app state
Build Vite 6 Frontend build tooling

Prerequisites

Tool Version Install
Go 1.22+ go.dev/dl
Node.js 20+ nodejs.org
Wails CLI v3 go install github.com/wailsapp/wails/v3/cmd/wails3@latest

Quick Start

# Clone
git clone https://github.com/vstockwell/wraith.git
cd wraith

# Install frontend dependencies
cd frontend && npm install && cd ..

# Run in dev mode (hot-reload frontend + Go backend)
wails3 dev

The app opens a 1400x900 window. On first launch you will be prompted to create a master password for the vault.

Building

# Production build for Windows
wails3 build

# Output: build/bin/wraith.exe

The build embeds the compiled frontend (frontend/dist) into the Go binary via //go:embed. Ship wraith.exe alongside freerdp3.dll for RDP support.

Project Structure

wraith/
  main.go                          # Entry point -- Wails app setup, service registration
  go.mod                           # Go module (github.com/vstockwell/wraith)
  internal/
    app/
      app.go                       # WraithApp -- wires all services, vault create/unlock
    db/
      sqlite.go                    # SQLite open with WAL mode, busy timeout, FK enforcement
      migrations.go                # Embedded SQL migration runner
      migrations/
        001_initial.sql            # Schema: groups, connections, credentials, ssh_keys,
                                   #         themes, host_keys, connection_history, settings
    vault/
      service.go                   # Argon2id key derivation + AES-256-GCM encrypt/decrypt
    connections/
      service.go                   # Connection and Group CRUD (hierarchical tree)
      search.go                    # Full-text search + tag filtering via json_each()
    settings/
      service.go                   # Key-value settings store (vault salt, preferences)
    theme/
      builtins.go                  # 7 built-in theme definitions
      service.go                   # Theme CRUD + idempotent seeding
    session/
      session.go                   # SessionInfo struct + state machine (connecting/connected/detached)
      manager.go                   # Concurrent session manager -- create, detach, reattach, 32-session cap
    plugin/
      interfaces.go                # ProtocolHandler + Importer + Session interfaces
      registry.go                  # Plugin registry -- register/lookup protocol handlers and importers
  frontend/
    package.json                   # Vue 3, Pinia, Naive UI, Tailwind CSS, Vite
    vite.config.ts                 # Vite + Vue + Tailwind plugin config
    src/
      main.ts                      # App bootstrap -- createApp, Pinia, mount
      App.vue                      # Root component
      layouts/
        MainLayout.vue             # Sidebar + tab bar + session area + status bar
        UnlockLayout.vue           # Master password entry screen
      components/
        sidebar/
          ConnectionTree.vue       # Hierarchical connection/group tree
          SidebarToggle.vue        # Collapse/expand sidebar
        session/
          TabBar.vue               # Draggable session tabs
          SessionContainer.vue     # Active session viewport
        common/
          StatusBar.vue            # Bottom status bar
      stores/
        app.store.ts               # Global app state (sidebar, vault status)
        connection.store.ts        # Connection + group state
        session.store.ts           # Active sessions state
  images/
    wraith-logo.png                # Application logo

Architecture

Go Backend                    Wails v3 Bindings              Vue 3 Frontend
(services + business logic)   (type-safe Go <-> JS)          (WebView2)
                                     |
  WraithApp ─────────────────────────┼──────────────> Pinia Stores
    |-- VaultService                 |                   |-- app.store
    |-- ConnectionService            |                   |-- connection.store
    |-- ThemeService                 |                   |-- session.store
    |-- SettingsService              |                        |
    |-- SessionManager               |                   Vue Components
    |-- PluginRegistry               |                   |-- MainLayout
                                     |                   |-- ConnectionTree
  SQLite (WAL mode)                  |                   |-- TabBar
    wraith.db                        |                   |-- SessionContainer
    %APPDATA%\Wraith\                |                   |-- StatusBar

How it fits together:

  1. main.go creates a WraithApp and registers Go services as Wails bindings.
  2. Wails generates type-safe JavaScript bindings so the Vue frontend can call Go methods directly.
  3. The Vue frontend uses Pinia stores to manage reactive state, calling into Go services for all data operations.
  4. All secrets (passwords, SSH keys) are encrypted with AES-256-GCM before being written to SQLite. The encryption key is derived from the master password using Argon2id and is never persisted.
  5. Sessions are managed by the Go SessionManager -- they are decoupled from windows, enabling tab detach/reattach without dropping connections.

Data storage: SQLite with WAL mode at %APPDATA%\Wraith\wraith.db (Windows) or ~/.local/share/wraith/wraith.db (Linux/macOS dev). Foreign keys enforced, 5-second busy timeout.

Plugin Development

Wraith uses a plugin registry with two interfaces: ProtocolHandler for new connection protocols and Importer for loading connections from external tools.

Implementing a ProtocolHandler

package myplugin

import "github.com/vstockwell/wraith/internal/plugin"

type MyProtocol struct{}

func (p *MyProtocol) Name() string { return "myproto" }

func (p *MyProtocol) Connect(config map[string]interface{}) (plugin.Session, error) {
    // Establish connection, return a Session
}

func (p *MyProtocol) Disconnect(sessionID string) error {
    // Clean up resources
}

Implementing an Importer

package myplugin

import "github.com/vstockwell/wraith/internal/plugin"

type MyImporter struct{}

func (i *MyImporter) Name() string                  { return "myformat" }
func (i *MyImporter) FileExtensions() []string       { return []string{".myconf"} }
func (i *MyImporter) Parse(data []byte) (*plugin.ImportResult, error) {
    // Parse file bytes into ImportResult (groups, connections, host keys, theme)
}

Registering Plugins

Register handlers and importers with the plugin registry during app initialization:

app.Plugins.RegisterProtocol(&myplugin.MyProtocol{})
app.Plugins.RegisterImporter(&myplugin.MyImporter{})

The ImportResult struct supports groups, connections, host keys, and an optional theme -- everything needed to migrate from another tool in a single import.

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/my-feature)
  3. Make your changes
  4. Run tests: go test ./...
  5. Run frontend checks: cd frontend && npm run build
  6. Commit and push your branch
  7. Open a Pull Request

License

MIT -- Copyright (c) 2026 Vantz Stockwell