tailminal
v0.2.2
Published
Use the terminal of any device in your Tailscale tailnet from any other device. Persistent PTY sessions, agent-friendly CLI, web UI on every node.
Downloads
506
Maintainers
Readme
⚡ Tailminal
Use the terminal of any device in your Tailscale tailnet from any other device — manually via a web UI, interactively via CLI, or programmatically from agents (plain stdin/stdout subprocess).
┌─────────────┐ HTTP + WebSocket ┌─────────────┐
│ PC (client)│◄─────────────────────►│ Laptop │
│ tailminal │ over tailnet │ (server) │
└─────────────┘ └─────────────┘
Every node runs the same single binary: server + CLI + web UI.
No central hub. Discovery via MagicDNS.Features
- One-shot exec — run a command on any node, stream stdout/stderr, get the exit code
- Persistent PTY sessions — interactive shells that survive disconnects; reattach with scrollback replay
- Agent-friendly CLI —
tailminal exec/tailminal attachare plain stdin/stdout subprocesses any agent framework can spawn - Web UI on every node — host list, live xterm.js terminal, one-shot command form
- Cross-platform — Windows (PowerShell/CMD), macOS (zsh), Linux (bash/sh)
- Zero-setup tailnet access — Tailscale and loopback clients connect without another credential
- Automatic discovery — online tailnet devices appear without editing a peer list, with Tailminal availability shown separately
Install
npm install -g tailminalOr from source:
git clone https://github.com/TheReal-Flo/tailminal
cd tailminal && pnpm install && pnpm buildStart the node server (generates ~/.tailminal/config.json on first run):
tailminal serve # listens on 0.0.0.0:7601Configuration — ~/.tailminal/config.json
{
"port": 7601,
"auth": "tailnet",
"discoverPeers": true,
"sessionTTL": "persistent",
"peers": [
{ "name": "laptop", "address": "laptop.your-tailnet.ts.net" }
]
}| Key | Default | Meaning |
| ------------ | ------------- | -------------------------------------------------------------- |
| port | 7601 | Fixed listen port for all nodes |
| auth | tailnet | Passwordless access from loopback/Tailscale addresses; use token for legacy bearer auth |
| discoverPeers | true | Discover online Tailnet devices and check Tailminal availability |
| sessionTTL | persistent | Sessions live until reboot; or auto-reap detached ones (30m, 12h, 7d, …) |
| shell | platform default | Override the PTY login shell executable |
| peers | [] | MagicDNS hostnames shown in the host list |
With the defaults, no peer list or token setup is needed. Tailminal reads the
local tailscale status, adds online peers automatically, and indicates whether
Tailminal is running on port 7601. Configured peers remain useful for aliases or devices
that cannot be discovered. Requests from
LAN and public addresses are rejected even though the server listens on all
interfaces. To opt into the old credential flow, set "auth": "token"; the
server then creates ~/.tailminal/token, and clients can use that file or
TAILMINAL_TOKEN.
CLI
tailminal serve Start the node server
tailminal hosts List configured peers + status
tailminal exec <host> [opts] -- <cmd...> One-shot command (streams output, propagates exit code)
--cwd <dir> Remote working directory
--shell <auto|powershell|cmd|sh> Shell preference (Windows: powershell/cmd)
--timeout-ms <n> Kill after n ms
tailminal attach <host> [session-id] Interactive PTY over stdio (Ctrl+] detaches, session stays alive)
tailminal sessions <host> List remote sessions
tailminal token Print the token when using auth: "token"Hosts can be MagicDNS names (laptop), full URLs (http://laptop:7601), or
peer names from your config.
Agent usage
Agents need zero integration — just spawn a subprocess:
tailminal exec laptop -- adb devices # exit code propagates
tailminal attach laptop # raw bidirectional shell on stdin/stdoutWeb UI
Open http://<node>:7601 over Tailscale, pick a host, and get a live terminal.
No credential prompt appears in the default tailnet mode. Device names can be
renamed with the edit button; aliases are stored only in that browser and do
not change Tailscale or Tailminal configuration.
Autostart
Windows (Task Scheduler, run at logon):
schtasks /create /tn Tailminal /tr "node C:\path\to\apps\cli\dist\index.js serve" /sc onlogonLinux (systemd user service, ~/.config/systemd/user/tailminal.service):
[Unit]
Description=Tailminal node server
[Service]
ExecStart=/usr/bin/node /opt/tailminal/apps/cli/dist/index.js serve
Restart=on-failure
[Install]
WantedBy=default.targetmacOS (launchd, ~/Library/LaunchAgents/dev.tailminal.plist): use
RunAtLoad with the same node command.
Monorepo layout
| Package | Purpose |
| --------------------- | ------------------------------------------------ |
| packages/shared | Zod protocol types shared by all packages |
| packages/server | Fastify server, PTY manager, auth, static web UI |
| packages/client | HTTP + WebSocket client library |
| packages/web (ui-src in server) | xterm.js UI bundled to static/ |
| apps/cli | tailminal binary |
| website/ | Static marketing site + /llms.txt agent docs |
Development
pnpm build # build all packages (topological)
pnpm typecheck # strict tsc across the workspace
pnpm test # vitest unit testsWebsite
website/ is a dependency-free static site. The human-facing page is
index.html; agents are pointed at /llms.txt (via an HTML comment,
<link rel="llms-txt">, and a footer note) which contains machine-readable
setup and API documentation. Serve the folder with any static file server, or
point the node server at it:
TAILMINAL_STATIC=./website tailminal serve