Hit localhost:3000 as if it were always running — even when the real service lives on another machine.
mole opens one SSH connection to a remote host and forwards local TCP
ports through it to the same ports on the remote. Turn on auto-discover and it
figures out what's actually listening on the remote and forwards it for
you — no port list to maintain. Run it in the background and watch it pick up
new dev servers as you start them.
┌────────────┐ one SSH connection ┌──────────────────┐
│ browser │ ──► localhost:3000 ──►──────►│ workstation:3000│
│ curl │ ──► localhost:5173 ──►──────►│ workstation:5173│
│ whatever │ ──► localhost:8080 ──►──────►│ workstation:8080│
└────────────┘ ▲ └──────────────────┘
│ mole runs here, on your laptop
| 🔌 One connection | A single SSH client multiplexes every forwarded port. |
| 🔭 Smart auto-discover | Enumerates the remote's real TCP listeners (ss/netstat) and forwards them — any port, not a fixed list. Re-scans every 15s, so servers you start after launch get picked up. |
| 🧭 SSH config aliases | A remote like dev is resolved through ~/.ssh/config (ssh -G): HostName, User, Port, IdentityFile, Include, Match — all honoured. |
| 🛡️ Exclude list | System/reserved ports (22, 25, 53, 111, 631) are skipped by default; fully configurable. |
| ♻️ Auto-reconnect | Transparent reconnect on tunnel drop, with periodic health checks. |
| 🔑 Native auth | ssh-agent first (Unix socket / Windows named pipe), then ~/.ssh/id_* keys. |
| 🌙 Background daemon | mole up -d detaches; mole down stops it; mole status and mole logs introspect it. |
| 🎨 Beautiful logs | mole logs renders the daemon log with colour level badges, a green FORWARD badge, and (×N) collapsing of repeats. |
| 📦 Single binary | No runtime, no node_modules, no background service to manage. |
mole logs renders the daemon's structured log with coloured level badges, a
distinct green FORWARD badge when a port starts forwarding and a burnt-orange
UNFWD badge when a dead remote port is pruned, dimmed timestamps, and
(×N) collapsing of repeated lines:
# 1. install
curl -fsSL https://raw.githubusercontent.com/Luqueee/mole/main/scripts/install.sh | sh
# 2. configure + start (interactive; accepts an ssh config alias as the remote)
mole init
# 3. later…
mole status # what's forwarded right now
mole logs -f # pretty, colourised, live
mole down # stop the background daemonmole init writes the config, then offers to start mole in the background
for you. From there it keeps the tunnel up and forwards new servers as they
appear.
| Command | What it does |
|---|---|
mole up |
Start the forwarder in the foreground (add -d to background it). |
mole down |
Stop a backgrounded mole (started with up -d). |
mole restart |
Stop and re-launch a backgrounded mole using the same config. |
mole status |
Query the local admin API for live stats + forwarded ports. |
mole logs |
Show the daemon log, colourised; -f to follow, clean to truncate it. |
mole ports |
Manage the auto-discover port list: add, remove/rm, list/ls. |
mole config |
config edit opens the active mole.yaml in $VISUAL / $EDITOR. |
mole init |
Generate a mole.yaml interactively (or scripted). |
mole clip |
Share clipboard images over the tunnel: clip serve, clip pull. |
mole update |
Update mole in place to the latest release (re-runs the installer). |
mole version · mole help |
The obvious. |
Five ways — pick whichever fits. All produce the same single static binary.
The installer detects the platform, builds the binary, and copies it onto your PATH.
Linux / macOS / FreeBSD
curl -fsSL https://raw.githubusercontent.com/Luqueee/mole/main/scripts/install.sh | shWindows (PowerShell 5+)
iwr -useb https://raw.githubusercontent.com/Luqueee/mole/main/scripts/install.ps1 | iexDefault install locations:
| User | Install location |
|---|---|
| root (Unix) | /usr/local/bin/mole |
| non-root (Unix) | ~/.local/bin/mole |
| Windows | %LOCALAPPDATA%\Programs\mole\ |
If the destination isn't on your PATH, the script prints the exact line to add to your shell profile.
Useful flags:
./scripts/install.sh --init # also launch the configurator
./scripts/install.sh --prefix /opt # custom prefix
MOLE_VERSION=v0.1.0 ./scripts/install.sh # pin a ref
.\scripts\install.ps1 -InstallDir $env:LOCALAPPDATA\Programs\mole # Windows custom dir
.\scripts\install.ps1 -Init # Windows + initgo install github.com/Luqueee/mole/cmd/mole@latest
# ensure the dir is on PATH:
export PATH="$(go env GOPATH)/bin:$PATH"git clone https://github.com/Luqueee/mole && cd mole
./scripts/install.sh # Unix (./scripts/install.ps1 on Windows)Override the destination with --prefix or INSTALL_DIR:
./scripts/install.sh --prefix /opt # → /opt/bin/mole
INSTALL_DIR=~/bin/mole ./scripts/install.sh # → ~/bin/molegit clone https://github.com/Luqueee/mole && cd mole
make install # → $(go env GOPATH)/bin/mole
make install PREFIX=/usr/local # → /usr/local/bin/mole
make install INSTALL_DIR=~/bin/mole # → ~/bin/moleFor CI, dotfiles, Dockerfiles, and curl | sh lovers: install the binary and
generate a working config in one non-interactive pass. With -no-prompt,
mole init reads its answers from MOLE_* env vars, so there's nothing to type.
The installer auto-detects the non-TTY stdin and forwards -no-prompt for you.
curl -fsSL https://raw.githubusercontent.com/Luqueee/mole/main/scripts/install.sh \
| MOLE_REMOTE=dev@workstation \
MOLE_AUTO_DISCOVER=true \
sh -s -- --initAdd MOLE_GLOBAL=true to write ~/.config/mole/config.yaml (per-user) instead
of ./mole.yaml (per-project). Pin a ref with MOLE_VERSION=v0.1.0. On Windows,
set $env:MOLE_* and run iwr … | iex -Init.
make build # → ./dist/mole
# or: go build -trimpath -o ./mole ./cmd/moleUpdate an installed mole in place — no manual re-clone. It re-runs the official installer against the running binary's own location:
mole update # update to the latest main
mole update -version v0.2.0 # pin a specific git ref (branch, tag, or commit)
mole update -dry-run # print what it would run, change nothingNeeds go plus curl/wget (Unix) or PowerShell (Windows). For a go install
setup, re-run go install github.com/Luqueee/mole/cmd/mole@latest instead.
./scripts/uninstall.sh # Unix (./scripts/uninstall.ps1 on Windows)
curl -fsSL https://raw.githubusercontent.com/Luqueee/mole/main/scripts/uninstall.sh | sh
./scripts/uninstall.sh --purge # also drop ~/.config/mole/
make uninstallFor go install, just remove $(go env GOPATH)/bin/mole.
mole up # foreground (Ctrl+C to stop) — great for debugging
mole up -d # background daemon — returns your shell
mole down # stop the daemonThe remote can be an explicit target or an SSH config Host alias:
mole up --remote dev@workstation # explicit user@host[:port]
mole up --remote dev # alias from ~/.ssh/config (resolved via ssh -G)mole up --remote dev --auto-discover # forward whatever's listening on the remote
mole up --remote dev --ports 3000,5173,8080 # forward an explicit setWith --auto-discover, mole enumerates the remote's TCP listeners and forwards
the loopback-reachable ones, skipping the configured exclude_ports. It re-scans
every 15 seconds, so a dev server you start later is forwarded automatically —
no restart needed.
Drop a mole.yaml in your project (or generate it with mole init):
remote: dev # user@host[:port] or an ssh config alias
auto_discover: true
exclude_ports: [22, 25, 53, 111, 631] # never auto-forwarded; [] excludes nothing
admin_addr: 127.0.0.1:9999
log_level: infomole up looks for ./mole.yaml first, then the user-global
~/.config/mole/config.yaml, so a config written by mole init -global
is picked up from anywhere.
mole status # JSON: uptime, connections, and the live forwarded-port set
mole logs # pretty, colourised log (last 200 lines)
mole logs -f # follow, like tail -f
mole logs -n 50 --no-dedupmole status needs the admin API to be on. It's off by default — set
admin_addr: 127.0.0.1:9999 in the config (mole init writes it for you) or
pass mole up -admin 127.0.0.1:9999.
mole logs parses the daemon's structured log and renders it with coloured
level badges (a distinct green FORWARD badge for forwarded ports), a dimmed
timestamp, and collapses consecutive identical lines into one with a (×N)
counter. Colour auto-disables when piped or when NO_COLOR is set (--color
forces it back on).
mole restart # stop and re-launch the daemon with the same config
mole ports list # show the auto-discover port list
mole ports add 4321 # add a port (live: applied to a running daemon too)
mole ports remove 4321 # or: mole ports rm 4321
mole config edit # open the active mole.yaml in $VISUAL / $EDITOR
mole config edit -editor nvim
mole logs clean # truncate the daemon log
mole logs clean -keep 200 # …keeping the last N linesmole clip moves clipboard images from the machine running clip serve to
the one running clip pull — useful for pasting a screenshot taken on a remote
desktop. Clipboard watching is macOS-only; other platforms can still serve
explicit pushes and pull.
mole clip serve # on the source machine (default: loopback 127.0.0.1:7777)
mole clip pull # on the target; uses clip_url from the config, or -urlThe clip endpoint has no authentication. The default loopback bind keeps it
off the network; for a remote pull, bind to the source machine's private
WireGuard or Tailscale address explicitly and use the same address in
clip_url:
clip_url: http://100.64.0.10:7777
clip_listen: 100.64.0.10:7777Binding to 0.0.0.0:7777, :7777, or [::]:7777 is supported for controlled
networks, but mole clip serve emits a warning because every interface can
reach the unauthenticated endpoint.
mole init ships inside the binary — no separate script — so the prompts
are identical on every OS and always in sync with the loader.
| Mode | When | How |
|---|---|---|
| Interactive | first-time setup | mole init |
| Semi-interactive | you know the remote | mole init -remote dev |
| Fully scripted | CI, Docker, no TTY | mole init -no-prompt -remote … [-auto-discover] |
$ mole init
configuring mole — press Enter to accept the default in [brackets]
SSH remote (user@host[:port] or ssh config alias): dev
How should mole pick ports?
1) auto-discover common dev ports (recommended)
2) explicit list (comma-separated)
3) skip — I'll configure ports later
choose [1]: 1
Where to save the config?
1) ./mole.yaml (current directory, project-local)
2) ~/.config/mole/config.yaml (user-global)
3) don't save — print to stdout instead
choose [1]: 2
wrote ~/.config/mole/config.yaml
Start mole now? [Y/n]: y
starting mole in the background
Useful init flags: -global, -print, -no-prompt, -yes, -test,
-force, -up. Environment fallbacks (read when the matching flag is empty):
MOLE_REMOTE, MOLE_PORTS, MOLE_AUTO_DISCOVER, MOLE_CONFIG_PATH,
MOLE_GLOBAL.
mole up [flags]
-config path to YAML config (default: ./mole.yaml, then user-global)
-remote SSH target (user@host[:port]) or an ssh config alias
-ports comma-separated ports to forward (e.g. 3000,5173)
-auto-discover forward whatever is listening on the remote
-admin admin HTTP address (empty to disable)
-log-level debug|info|warn|error
-insecure disable SSH host key verification (UNSAFE; dev only)
-d, -detach run in the background; stop with 'mole down'
mole down
mole restart [-config PATH]
mole status [-admin 127.0.0.1:9999]
mole logs [-f] [-n N] [-raw] [-color] [-no-color] [-no-dedup]
mole logs clean [-keep N]
mole ports add <port> [-config PATH]
mole ports remove|rm <port> [-config PATH]
mole ports list|ls [-config PATH]
mole config edit [-config PATH] [-editor CMD]
mole init [flags]
mole clip serve [-listen 127.0.0.1:7777] [-watch] [-config PATH] [-log-level L]
mole clip pull [-url URL] [-config PATH] [-log-level L]
mole update [-version REF] [-dry-run] [-no-verify]
mole version · mole help
| Field | Type | Default | Notes |
|---|---|---|---|
remote |
string | — | user@host[:port] or an ssh config alias (required) |
ports |
int[] | [] |
Explicit ports — always forwarded |
auto_discover |
bool | false |
Forward the remote's live listeners |
discover_ports |
int[] | see below | Fallback probe list when ss/netstat are absent |
exclude_ports |
int[] | [22,25,53,111,631] |
Never auto-forwarded; [] excludes nothing |
admin_addr |
string | "" (disabled) |
Admin HTTP address; mole init writes 127.0.0.1:9999. Required by mole status |
log_level |
string | info |
debug, info, warn, error |
ssh_port |
int | 22 |
SSH port on the remote |
insecure |
bool | false |
Disable SSH host key verification (UNSAFE; dev only) |
clip_url |
string | — | Clip server URL used by mole clip pull |
clip_listen |
string | 127.0.0.1:7777 |
Bind address; use a private Tailscale/WireGuard IP for remote access |
clip_interval_ms |
int | — | Clipboard poll interval for clip serve -watch |
Fallback discover_ports:
3000 3001 3002 3003 3004 3005
4200 5173 5174 5327
6006 8000 8080 8081 8443 9000 9090
- Open one SSH client connection to the remote (ssh-agent or
~/.ssh/id_*keys; aliases resolved viassh -G). - Discover ports: enumerate the remote's TCP listeners (
ss/netstat), or fall back to probingdiscover_ports. Skipexclude_ports. - For each port, bind
127.0.0.1:<port>locally; on a local connection, dial127.0.0.1:<port>through the tunnel and bridge bytes both ways. - Re-discover every 15s and forward anything new; a watchdog goroutine reconnects the SSH session if it dies.
State (pidfile + background log) lives in ~/.local/state/mole/ on Unix
(honouring XDG_STATE_HOME) and %LOCALAPPDATA%\mole\ on Windows.
Single static Go binary — Linux, macOS, Windows, FreeBSD (amd64 & arm64). SSH auth is native per platform:
- Linux / macOS / BSD — ssh-agent over a Unix socket (
SSH_AUTH_SOCK). - Windows — ssh-agent over the OpenSSH named pipe
(
\\.\pipe\openssh-ssh-agent).
- Host keys are verified against
~/.ssh/known_hosts. Unknown hosts are trusted on first use (recorded automatically, like OpenSSH); a later key mismatch is refused as a possible MITM. Pass--insecure(orinsecure: true) to turn verification off for throwaway dev hosts. - Dead ports are pruned only under auto-discover — when
ss/netstatcan be enumerated, a remote service that stops has its local listener closed (you'll see anUNFWDline). Explicitly configuredports:are pinned and never pruned, and when enumeration isn't available mole only adds, never removes. - TCP only — no UDP forwarding yet.
- Pageant (PuTTY's Windows agent) isn't supported — OpenSSH agent only.
make build # → ./dist/mole
make install # build + install (PREFIX=/usr/local or INSTALL_DIR=… to override)
make run # build, then run `mole up`
make test # go test ./...
make tidy
make uninstall
make cleanMIT.
