A local-first session terminal platform. Hub daemon + remote agents + SSH transport + PWA UI. Sessions survive client disconnects and device switches; local sessions also survive hub restarts.
Not yet published to npm. Under active development.
- Session persistence — terminal sessions outlive client/UI disconnects and device switches; reconnect and resume exactly where you left off. Local sessions also survive hub restarts. (Surviving a dropped SSH transport to a remote host — so a remote session keeps running across a network blip — is on the roadmap, not yet shipped.)
- SSH stdio transport — agents run over SSH stdio; no extra ports opened on remote machines
- Local-first — the hub daemon owns all state; the UI is a thin client that can come and go
- Multi-device — attach to the same session from any browser or device; write-lock prevents conflicting input
- Remote agents — same agent binary runs locally (child process) or remotely (SSH); hub handles both identically
- Custom themes — per-host and per-channel visual identity with Discord-style host rail
- Launch profiles — named shell configurations with environment, working directory, and elevation settings
- Elevation support — configurable elevation methods per host (sudo, doas, pkexec, gsudo, custom)
- Config cascade — 4-layer deep merge: built-in defaults →
config.toml→ host profile → channel profile
Build it from this repository:
pnpm install
./scripts/build-agent.sh # Rust agent → dist/sea/lasterm-agent
./scripts/build-hub.sh # hub SEA → dist/sea/lasterm-hub
./dist/sea/lasterm-hub startBoth binaries, in that order: build-hub.sh does not build the agent, and the hub
resolves it next to its own executable. Without it the UI serves but the first local
terminal fails. On Windows use scripts\build-agent.ps1 and scripts\build-hub.ps1.
Open http://localhost:4100 in your browser.
There is no npx lasterm, and there will not be one under that name: the unscoped
lasterm on npm belongs to an unrelated project. The hub ships as a single
executable that embeds its own Node, so npm would add a runtime requirement it
exists to remove. Packaged builds land in the releases.
UI (Vue 3 + xterm.js) ──── WS + REST ──── Hub (Fastify, 127.0.0.1:4100)
├── Local Agent (child_process, stdio)
├── Remote Agent (ssh2, stdio)
├── meta.db (hosts, sessions, workspaces)
└── spool.db (output chunks, snapshots)
Agent (local or remote, same binary):
stdin → MessagePack frames → PTY manager (async-xpty, Rust) → N channels
stdout ← MessagePack frames ← OUTPUT / SNAPSHOT
Hub never touches PTY directly — the agent is the universal PTY manager.
The hub daemon binds to 127.0.0.1:4100 and serves both the REST API (/api/*) and the WebSocket endpoint (/ws). All PTY management is delegated to the agent process, whether local or remote.
The hub bundles the agent binary for its own OS/arch (used for local sessions, available offline at
install). Agents for remote SSH hosts of other OS/arch are not bundled — the hub downloads the
matching, version-matched, checksum-verified binary from GitHub Releases on demand and uploads it to the host
over SFTP, so the remote host never needs outbound internet. Pre-populate with lasterm-hub agent fetch <os-arch> | --all.
Air-gapped note: this assumes the hub has outbound internet. If the hub itself is air-gapped, a fetch fails with an actionable message (download URL + cache path + filename); download the binary and its
SHA256SUMSon a connected machine, transfer them, and drop them in the binary cache. Seedocs/SPEC.md§3.5 for the full distribution model.
| Package | npm name | Description |
|---|---|---|
packages/shared |
@lasterm/shared |
Protocol types, MessagePack codec, entity types, config types |
crates/lasterm-agent |
a Rust binary | PTY manager — async-xpty + the vt100 crate + MessagePack framing |
packages/hub |
@lasterm/hub |
Fastify daemon — session manager, client manager, storage, SSH transport |
packages/clients/web |
@lasterm/web |
Vue 3 PWA — embedded in hub at build time, not published separately |
packages/clients/desktop |
@lasterm/desktop |
Tauri desktop app wrapping the hub as a sidecar (P1) |
| root | workspace root | CLI entrypoint — thin wrapper around @lasterm/hub. Not published |
- Node.js >= 24 LTS
- pnpm >= 9
# Install all dependencies
pnpm install
# Start hub + UI dev servers concurrently
pnpm devThe hub starts on http://localhost:4100 and the Vite dev server on http://localhost:5173.
pnpm build # Build all packages
pnpm test # Run all tests (vitest)
pnpm lint # Lint + format check (biome)
pnpm lint:fix # Auto-fix lint issues
# Single-package operations
pnpm -F @lasterm/hub test
pnpm -F @lasterm/web devUse the headless harness to exercise the hub WebSocket AUTH + SPAWN path without opening
the browser or touching your real Lasterm state:
scripts/dev/headless-hub-test.sh start # isolated hub on :4199 with debug logging enabled
scripts/dev/headless-hub-test.sh spawn # run AUTH + SPAWN probe
scripts/dev/headless-hub-test.sh logs # hub connection-lifecycle log tail
scripts/dev/headless-hub-test.sh alog # local agent daemon log tail
scripts/dev/headless-hub-test.sh stop
scripts/dev/headless-hub-test.sh resetThe harness writes all config, runtime, and state under .tt/headless-hub/. Override the
location or port with TT_DIR=/tmp/lasterm-headless or TT_PORT=4201. The hub's dev
agent resolver uses target/release/lasterm-agent, so rebuild that binary after Rust changes
before relying on the agent daemon log tail.
Build a self-contained release locally (Linux/macOS native):
./scripts/build-agent.sh # Rust agent → dist/sea/lasterm-agent (cargo --release)
./scripts/build-hub.sh # Hub SEA → dist/sea/lasterm-hub
# builds the web UI, embeds it, bundles better-sqlite3,
# and produces a Node Single Executable ApplicationBoth binaries land co-located in dist/sea/; the hub resolves the agent next to its own executable. Run it:
cd dist/sea
./lasterm-hub start --port 4100 # serves the PWA at http://127.0.0.1:4100 (add --daemon / --open)
./lasterm-hub pair # prints an 8-digit code to authorise a new browser client
./lasterm-hub status # or: ./lasterm-hub stopConfig lives in ~/.config/lasterm, runtime state in ~/.local/state/lasterm.
A native SEA embeds the host Node runtime, so build on the OS you target — a cross-platform binary (e.g. the Windows hub) must be produced on that platform.
The release workflow only builds and uploads the MSIX installer when the repository has the Partner Center
package identity configured as GitHub repository variables. Set these once under
Settings -> Secrets and variables -> Actions -> Variables:
| Variable | Value |
|---|---|
MSIX_IDENTITY_NAME |
Partner Center package/identity name |
MSIX_PUBLISHER |
Partner Center publisher subject, for example CN=... |
MSIX_PUBLISHER_DISPLAY_NAME |
Publisher display name shown for the app |
If any of these variables are unset, release.yml skips the MSIX build and upload but still ships the NSIS
and MSI desktop installers. The CI inspection build in build.yml still creates its placeholder MSIX artifact
unconditionally.
lasterm reads configuration from a TOML file:
- Linux / macOS:
~/.config/lasterm/config.toml - Windows:
%APPDATA%\lasterm\config.toml
State (databases, runtime socket) is stored in:
- Linux / macOS:
~/.local/state/lasterm/ - Windows:
%LOCALAPPDATA%\lasterm\
The port defaults to 4100 and can be overridden via:
- CLI flag
--port - Environment variable
LASTERM_PORT portkey inconfig.toml
Terminal background settings live in [terminal] and cascade to host/channel profiles:
[terminal]
wallpaper = "forest.webp"
wallpaper_blur = 4
wallpaper_dim = 25
# image = wallpaper when set, otherwise solid; solid = opaque theme background;
# transparent = desktop transparency in Tauri, solid fallback in browsers.
background_mode = "transparent"
# Desktop-only native effect. auto maps to blur on Windows 10, mica on Windows 11,
# vibrancy-under-window on macOS, and none on Linux.
window_effect = "auto"In zero_conf mode the hub auto-increments from 4100 to 4199 if the default port is taken, and writes the actual port to runtime.json in the state directory.
lasterm is licensed per component:
| Component | License |
|---|---|
lasterm (CLI), @lasterm/hub, @lasterm/web, @lasterm/desktop, crates/lasterm-agent |
AGPL-3.0-only |
@lasterm/shared |
MIT OR Apache-2.0 |
The async PTY library was extracted to its own repository, khiops/async-xpty (MIT OR Apache-2.0).
The application is AGPL so lasterm stays fully free software and self-hostable — including when run as a network service. The standalone libraries are permissively dual-licensed for ecosystem adoption.
This licensing is permanent. The launch license is a commitment, not a starting point: the application components will remain AGPL-3.0-only and the libraries will remain MIT OR Apache-2.0.
Contributions are accepted under the Developer Certificate of Origin (inbound = outbound). There is no CLA.