A TUI-first tool for managing Cloudflare Tunnels. Expose your local dev servers to the internet with custom subdomains on your own domains — add, edit, delete, start, and stop tunnels all from an interactive dashboard.
$ tuinnel
┌─────────────────┬──────────────────────────────────────┐
│ TUNNELS │ [1:Details] [2:Logs] [3:Metrics] │
│ ◉ app :3000 │ │
│ ◌ api :8080 │ Status: ◉ Connected Uptime: 00:12 │
│ │ Public: https://app.mysite.com │
└─────────────────┴──────────────────────────────────────┘
a Add d Delete e Edit s Start/Stop r Restart ? Help
- TUI dashboard — Full interactive dashboard; add, edit, delete tunnels without leaving the terminal
- Custom domains — Map local ports to subdomains on domains you own (
app.mysite.com,api.mysite.com) - Quick tunnels — Zero-config tunnels via
trycloudflare.com(no account needed) - Persistent state — Tunnels auto-restart between sessions based on last known state
- Inline onboarding — Setup wizard on first run, no separate CLI step needed
- Multi-tunnel — Run multiple tunnels simultaneously with a sidebar to switch between them
- Smart defaults — Auto-detects frameworks from
package.jsonand suggests subdomain names (port 4200 = "angular", 5173 = "vite", etc.) - Managed binary — Automatically downloads and manages the
cloudflaredbinary - Auto-HTTPS detection — Probes local ports to detect self-signed HTTPS and configures tunnels accordingly
- Atomic config — Config stored in
~/.tuinnel/config.jsonwith atomic writes and 0600 permissions - Diagnostics —
tuinnel doctorvalidates your token, permissions, binary, and network connectivity
- Node.js 20+ (runtime)
- macOS or Linux (darwin-arm64, darwin-x64, linux-arm64, linux-x64)
- A Cloudflare account with at least one domain (for named tunnels; quick tunnels work without an account)
npm install -g tuinnel# Open interactive dashboard (first run shows setup wizard)
tuinnel
# Quick start a tunnel on port 3000
tuinnel 3000Expose a local server with a random public URL:
tuinnel up 3000 --quick
# => https://random-words.trycloudflare.com <- :3000# 1. Open tuinnel — the setup wizard runs automatically on first launch
tuinnel
# 2. Or set up via CLI (one-time)
tuinnel init
# 3. Start a tunnel via CLI
tuinnel up 3000
# => https://app.mysite.com <- :3000tuinnel requires a scoped API token (not a Global API Key) with specific permissions. Follow these steps to create one:
Go to https://dash.cloudflare.com/profile/api-tokens and click Create Token.
Scroll past the pre-built templates and click Create Custom Token at the bottom of the page. Give it a descriptive name like tuinnel or tunnel-manager.
Add these three permissions. All three are required:
| Permission | Access | Why it's needed |
|---|---|---|
| Zone > Zone > Read | Read | List your domains so tuinnel can discover zones and account IDs |
| Zone > DNS > Edit | Edit | Create and delete CNAME records that point subdomains to your tunnels |
| Account > Cloudflare Tunnel > Edit | Edit | Create, configure, and delete named tunnels |
Your permissions table should look like this:
+----------+-------------------+------+
| Zone | Zone | Read |
| Zone | DNS | Edit |
| Account | Cloudflare Tunnel | Edit |
+----------+-------------------+------+
Under Zone Resources, choose which zones (domains) the token can access:
- All zones — If you want tuinnel to work with any domain in your account
- Specific zone — If you want to restrict the token to a single domain (recommended for tighter security)
Under Account Resources, select the account that owns your zones.
For additional security, you can restrict the token to only work from your IP address or IP range under Client IP Address Filtering. This is optional but recommended for production use.
You can set a start and end date for the token. Leave blank for a non-expiring token.
Click Continue to summary, review the permissions, then click Create Token.
Copy the token immediately. Cloudflare will only show it once. If you lose it, you'll need to create a new one.
Run tuinnel and the setup wizard will guide you through token configuration:
tuinnelOr use the CLI setup command:
tuinnel initWhen prompted, paste your token. tuinnel will:
- Validate the token against the Cloudflare API
- List your available zones (domains)
- Let you pick a default zone
- Save the config to
~/.tuinnel/config.json
Instead of storing the token in the config file, you can set it as an environment variable:
export CLOUDFLARE_API_TOKEN="your-token-here"Or use the tuinnel-specific variable:
export TUINNEL_API_TOKEN="your-token-here"Environment variables take priority over the config file.
Run diagnostics to confirm everything is set up correctly:
tuinnel doctorExpected output:
tuinnel doctor
PASS Config file
Found at ~/.tuinnel/config.json
PASS API token
Token found (config file, ending ...ab1c)
PASS Token validates
Valid. Access to 2 zones
PASS cloudflared binary
Version 2025.8.0 (managed)
PASS Network connectivity
Cloudflare API reachable (HTTP 200)
All 5 checks passed.
| Symptom | Cause | Fix |
|---|---|---|
Authentication failed |
Token is invalid, revoked, or expired | Create a new token at the API tokens page |
Insufficient permissions |
Token is missing one or more required permissions | Edit the token and add the missing permission (Zone:Read, DNS:Edit, or Cloudflare Tunnel:Edit) |
No zones found |
Token doesn't have access to any zones | Edit the token's Zone Resources to include your domain |
This looks like a Global API Key |
You pasted the 37-character Global API Key instead of a scoped token | Go to the API tokens page and create a new API Token (not the Global API Key shown at the top) |
| Command | Description |
|---|---|
tuinnel |
Open interactive TUI dashboard |
tuinnel <port> |
Quick start: create tunnel and open dashboard |
tuinnel init |
Interactive setup wizard for API token and default zone |
tuinnel up [ports...] |
Start tunnels and open the TUI dashboard |
tuinnel up <port> --quick |
Start an ephemeral quick tunnel (no account needed) |
tuinnel up <port> --no-tui |
Start tunnels with plain log output instead of TUI |
tuinnel down [names...] |
Stop running tunnels |
tuinnel down --all |
Stop all running tunnels |
tuinnel down <name> --clean |
Stop a tunnel and delete its DNS record and tunnel from Cloudflare |
tuinnel add <port> |
Add a tunnel mapping to config (does not start it) |
tuinnel add <port> --subdomain <name> |
Add with explicit subdomain (non-interactive) |
tuinnel add <port> --adopt |
Adopt an existing Cloudflare tunnel |
tuinnel remove <name> |
Remove a tunnel mapping from config |
tuinnel list |
List all configured tunnels |
tuinnel status |
Show running tunnels with health status |
tuinnel zones |
List available Cloudflare zones (domains) |
tuinnel doctor |
Run diagnostics (token, permissions, binary, network) |
tuinnel purge |
Find and remove orphaned tunnels and DNS records |
| Alias | Equivalent |
|---|---|
tuinnel start |
tuinnel up |
tuinnel stop |
tuinnel down |
tuinnel rm |
tuinnel remove |
tuinnel ls |
tuinnel list |
| Flag | Scope | Description |
|---|---|---|
--verbose |
Global | Enable verbose output |
--json |
list, status, zones |
Output in JSON format |
-v, --version |
Global | Show version |
--help |
Global | Show help for any command |
| Key | Action |
|---|---|
q |
Quit (confirm: "Stop all tunnels and exit? Y/n") |
a |
Add new tunnel (opens wizard modal) |
? |
Full help overlay (dismissible with any key) |
Tab |
Switch focus: sidebar / main panel |
1 2 3 |
Switch main panel tab (Details / Logs / Metrics) |
Up / Down / k / j |
Navigate tunnel list |
c |
Copy public URL to clipboard |
o |
Open public URL in browser |
| Key | Action |
|---|---|
e |
Edit selected tunnel |
d |
Delete selected tunnel (with confirmation) |
s |
Start/stop selected tunnel |
r |
Restart selected tunnel |
| Key | Action |
|---|---|
Tab |
Next field |
Enter |
Confirm / Submit |
Esc |
Cancel / Close modal |
Up / Down |
Navigate select lists |
tuinnel uses a hybrid approach:
- Cloudflare REST API handles all CRUD operations — creating tunnels, managing DNS CNAME records, and configuring ingress rules
- cloudflared binary runs as a connector process that maintains the actual tunnel connection to Cloudflare's edge network
- Prometheus metrics are scraped from cloudflared's local metrics server for real-time dashboard stats (request counts, latency percentiles, connection health)
When you run tuinnel 3000:
- Opens the TUI dashboard
- Creates a named tunnel on Cloudflare (or reuses an existing one)
- Configures the tunnel's ingress rules to route traffic to
localhost:3000 - Creates a DNS CNAME record pointing your subdomain to the tunnel
- Spawns a
cloudflaredconnector process with live metrics and logs in the dashboard
Tunnels are named with a tuinnel- prefix on Cloudflare to avoid collisions with other tools.
Stored at ~/.tuinnel/config.json:
The lastState field tracks whether each tunnel was running or stopped when you last exited. Tunnels with lastState: "running" auto-start when you open the dashboard. The tunnelId field caches the Cloudflare tunnel UUID for faster restarts.
The cloudflared binary is managed at ~/.tuinnel/bin/cloudflared.
# Install dependencies
bun install
# Build
bun run build
# Build in watch mode
bun run dev
# Type-check
bunx tsc --noEmit
# Run tests
bun test
# Run a single test file
bun test tests/cloudflare/api.test.ts
# Smoke test the built CLI
node dist/index.js --helpMIT
{ "version": 1, "apiToken": "your-api-token", "defaultZone": "mysite.com", "tunnels": { "app": { "port": 3000, "subdomain": "app", "zone": "mysite.com", "protocol": "http", "lastState": "running", "tunnelId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }, "api": { "port": 8080, "subdomain": "api", "zone": "mysite.com", "protocol": "http", "lastState": "stopped" } } }