# Tunlite > **SSH tunnels for you and your Agent — kept alive.** Type the `-L`/`-R`/`-D` yourself, or > just tell an AI Agent — tunlite builds the tunnel and keeps it connected. **English** · [简体中文](README.zh-CN.md) [![CI](https://github.com/yuanyuanzijin/tunlite/actions/workflows/ci.yml/badge.svg)](https://github.com/yuanyuanzijin/tunlite/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/tunlite)](https://www.npmjs.com/package/tunlite) [![downloads](https://img.shields.io/npm/dm/tunlite)](https://www.npmjs.com/package/tunlite) [![node](https://img.shields.io/badge/node-%E2%89%A518-brightgreen)](https://nodejs.org) [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE) Keeping SSH tunnels alive is a chore: pros juggle `autossh`, a `systemd` unit per tunnel, and a pile of `-L`/`-R`/`-D` flags — and reconnect by hand when one drops; newcomers don't even know where to start. **tunlite** folds it into one command: type it yourself, or just tell an **AI Agent** in plain words — either way it builds the tunnel, keeps it alive, and reconnects on its own. Pure Node.js, **zero dependencies**, wrapping the `ssh` you already trust.

define a tunnel, the daemon brings it up, check status, tail logs

> 📖 **Full documentation → [tunlite.dev](https://tunlite.dev/)** - **Agent-native** — `--json` + stable exit codes on every command, plus a bundled Agent skill: an AI Agent sets up, brings up, and troubleshoots tunnels end-to-end. - **Zero third-party dependencies** — pure Node.js standard library; all it needs on the box is **Node ≥ 18** and the system `ssh` it wraps. - **Auto-reconnect** — exponential backoff + jitter, keepalive, port health probes. - **Start at login** — launchd (macOS) / systemd user service (Linux) / Task Scheduler (Windows — beta). - **Passwordless setup** — connects directly if keys already work; installs your key only if needed. - **Three forward types** — local `-L`, remote `-R`, dynamic SOCKS `-D`. ## For Agents An **AI Agent** is a first-class user. Ask one in plain language and it drives `tunlite` through the same `--json` surface you would — branching on exit codes, not scraping prose: ```text you ▸ "Forward the Postgres on app01 to my laptop." Agent ▸ tunlite add pg --to deploy@app01 -L 5432:localhost:5432 --json → {"ok":true,…} Agent ▸ tunlite enable pg --json → exit 4 · needs-auth Agent ▸ tunlite setup-key deploy@app01 → key installed Agent ▸ tunlite enable pg --json → {"state":"connected"} · exit 0 Agent ▸ "Done — localhost:5432 reaches app01's Postgres; the daemon keeps it alive." ``` The bundled [`skill/ssh-tunnel`](skill/ssh-tunnel/SKILL.md) (installed by `tunlite install skill`) tells an Agent exactly how: `--json`, branching on exit codes, and handling `needs-auth`. `tunlite monitor` gives you a live, top-style dashboard — every tunnel's state at a glance, with the daemon auto-reconnecting a dropped one in front of you:

tunlite monitor — live dashboard with auto-reconnect and per-tunnel detail

## Why tunlite? If you keep a few SSH tunnels running — a reverse tunnel to a homelab box, a SOCKS proxy through a bastion, a port-forward to a staging database — you've probably wired up `autossh` plus a `systemd`/`launchd` unit for each, and memorized which `-L`/`-R`/`-D` flag goes where. tunlite folds all of that into one declarative CLI on top of the `ssh` you already trust: named tunnels a daemon keeps alive and the OS restarts at boot — no new server, no account, no protocol. And because every command is `--json` with stable exit codes, an Agent drives the exact same surface you do. | | tunlite | autossh | plain `ssh -L/-R/-D` | sshuttle | frp · bore · chisel | ngrok | |---|:---:|:---:|:---:|:---:|:---:|:---:| | Agent-friendly (`--json`, stable exit codes) | ✅ | ❌ | ❌ | ❌ | ❌ | partial | | Wraps your system `ssh` (keys, jump hosts, `ssh_config`) | ✅ | ✅ | ✅ | partial | ❌ own protocol | ❌ own service | | Named, declarative tunnels | ✅ | ❌ | ❌ | ❌ | ✅ config | ✅ | | Auto-reconnect (backoff, keepalive, health) | ✅ | basic | ❌ | ❌ | ✅ | ✅ | | Start at login (launchd/systemd/Task Scheduler) | ✅ | DIY | DIY | DIY | DIY | ✅ | | Local **+** remote **+** dynamic SOCKS | ✅ | ✅ | ✅ | transparent proxy | varies | varies | | Zero deps · no server to run · self-hosted | ✅ | needs autossh | ✅ | needs python | needs a server | hosted/paid | ## Install Prerequisite: **Node ≥ 18** and the system `ssh`, both on your PATH. ```bash # Recommended — fetch + anchor (no global npm needed) npx tunlite install # Or a curl one-liner (just curl/wget + tar + node) curl -fsSL https://raw.githubusercontent.com/yuanyuanzijin/tunlite/master/bootstrap.sh | sh # Windows (PowerShell) — beta irm https://raw.githubusercontent.com/yuanyuanzijin/tunlite/master/bootstrap.ps1 | iex ``` `tunlite install` copies the runtime to a fixed directory and writes a launcher that **pins node's absolute path** (so switching nvm/fnm versions won't break it), then asks whether to register login autostart, install the Agent skill, and enable shell completion. Pass `-y` to say yes to all without prompting (for scripts/CI); with no `-y` and no terminal it just anchors. To set up one piece on its own, use `tunlite install service` / `install skill` / `install completion`. It also writes a short `tun` alias when that name is free. **Windows (autostart, launcher, PATH) is beta** — macOS/Linux are the CI-tested platforms. ## Quick start ```bash # ssh-native forward flags (repeatable — one tunnel can carry several): tunlite add web --to me@host -L 8080:localhost:80 # reach the server's :80 at localhost:8080 tunlite add rev --to me@host -R 9000:localhost:3000 # expose local 3000 as server:9000 tunlite add socks --to me@host -D 1080 # SOCKS5 proxy (local 1080) tunlite status # aligned table: NAME STATE HOST TYPE ROUTE PID UP RESTARTS tunlite logs web -f # follow logs tunlite doctor # health check: why a tunnel won't connect ``` > **Upgrading from 0.9.x?** 0.10.0 leans into native `ssh`, so a couple of commands take a > new shape — while your existing tunnel config keeps working, untouched. > - **Forwards now speak ssh's own flags** — `add web --to me@host -L 8080:localhost:80 -D 1080` > (repeatable; `set ` edits them in place). The earlier `add local/remote/dynamic` > form gives way to this. > - **Switching a tunnel on and off is now `enable` / `disable`** (it was `up`/`down`), each > naming what it acts on — a name, `--tag