Install
openclaw skills install cftunnelExpose local services to the internet via Cloudflare Tunnels. CLI (npx cftunnel) and Node.js library for creating tunnels, configuring ingress routes, managi...
openclaw skills install cftunnelExpose local services to the internet via Cloudflare Tunnels. Use this skill when you need to make a locally running application accessible at a public HTTPS URL.
Flow: Start a local service (e.g. on port 3000) → cftunnel creates a tunnel + DNS route → the service is live at https://hostname.domain.com.
Set these environment variables before running any command:
# Option A: API Key + Email (most common)
export CLOUDFLARE_API_KEY=<api-key>
export CLOUDFLARE_EMAIL=<account-email>
# Option B: API Token (scoped, if available)
export CLOUDFLARE_API_TOKEN=<api-token>
# Always required:
export CLOUDFLARE_ACCOUNT_ID=<account-id>
If a tunnel and cloudflared are already running (check with tunnel list), just add a route and DNS:
# 1. Add ingress route to existing tunnel
npx cftunnel route add <tunnel-id> --hostname <hostname> --service http://localhost:<port>
# 2. Create DNS CNAME
npx cftunnel dns create --zone-id <zone-id> --hostname <hostname> --tunnel-id <tunnel-id>
npx cftunnel quickstart \
--name <tunnel-name> \
--hostname <hostname> \
--service http://localhost:<port> \
--zone-id <zone-id>
Then run the connector: npx cftunnel run <tunnel-id>
import { createClient, quickstart } from 'cftunnel';
const client = createClient({ apiKey: '...', apiEmail: '...' });
const result = await quickstart(client, {
accountId: '...',
name: 'my-app',
hostname: 'app.example.com',
service: 'http://localhost:3000',
zoneId: '...',
});
console.log(result.run_cmd);
| Command | Purpose |
|---|---|
npx cftunnel tunnel list | List all tunnels. Find existing tunnel IDs and check status (healthy/down/inactive). |
npx cftunnel tunnel create <name> | Create a new tunnel. Returns tunnel ID and secret. Add --config-src local for YAML-managed config. |
npx cftunnel tunnel get <tunnel-id> | Get tunnel details including connection status. |
npx cftunnel tunnel delete <tunnel-id> | Delete a tunnel. Must have no active connections. |
npx cftunnel tunnel token <tunnel-id> | Get the token needed to run cloudflared. Returns run_cmd and install_cmd. |
| Command | Purpose |
|---|---|
npx cftunnel route list <tunnel-id> | Show current ingress rules. Always has a catch-all 404 as last rule. |
npx cftunnel route add <tunnel-id> --hostname <host> --service <url> | Add a route. Preserves existing routes, appends before catch-all. Optional --path for path filtering. |
npx cftunnel route remove <tunnel-id> --hostname <host> | Remove a route by hostname. |
npx cftunnel route set <tunnel-id> --route host1=svc1 --route host2=svc2 | Replace ALL routes. Use for bulk configuration. |
| Command | Purpose |
|---|---|
npx cftunnel dns create --zone-id <zid> --hostname <host> --tunnel-id <tid> | Create proxied CNAME pointing hostname to tunnel. Required for the hostname to resolve. |
npx cftunnel dns list --zone-id <zid> | List all DNS records in the zone. |
npx cftunnel dns delete <record-id> --zone-id <zid> | Delete a DNS record. |
| Command | Purpose |
|---|---|
npx cftunnel run <tunnel-id> | Run cloudflared in foreground. Auto-detects cloudflared from PATH or npm package. |
npx cftunnel run <tunnel-id> --install-service | Install cloudflared as a persistent system service (survives reboots). |
"I need to expose port N on a domain"
→ Check tunnel list for a healthy tunnel. If one exists, use route add + dns create. If not, use quickstart.
"I need to add another service to an existing tunnel"
→ route add + dns create. One tunnel can serve many hostnames.
"I need to change where a hostname points"
→ route remove --hostname X then route add --hostname X --service <new-url>. DNS stays the same.
"I need to take a service offline"
→ route remove --hostname X and optionally dns delete <record-id>.
"The tunnel exists but cloudflared isn't running"
→ npx cftunnel run <tunnel-id> or use tunnel token to get the token for manual cloudflared invocation.
The --service flag accepts these protocols:
| Format | Example | Use case |
|---|---|---|
http://host:port | http://localhost:3000 | HTTP web apps, APIs |
https://host:port | https://localhost:8443 | HTTPS backends |
tcp://host:port | tcp://localhost:5432 | Databases, raw TCP |
ssh://host:port | ssh://localhost:22 | SSH access |
unix:///path | unix:///tmp/app.sock | Unix socket apps |
http_status:CODE | http_status:404 | Static status response (catch-all) |
All commands output JSON to stdout. Progress/errors go to stderr.
Parse with jq:
TUNNEL_ID=$(npx cftunnel tunnel create my-app | jq -r '.id')
TOKEN=$(npx cftunnel tunnel token $TUNNEL_ID | jq -r '.token')
cd /path/to/app && npm start &
npx cftunnel quickstart \
--name my-web-app \
--hostname app.example.com \
--service http://localhost:8080 \
--zone-id <zone-id>
npx cftunnel run <tunnel-id-from-output>
npx cftunnel tunnel list | jq '.[] | select(.status == "healthy")'
npx cftunnel route add <tunnel-id> --hostname api.example.com --service http://localhost:4000
npx cftunnel dns create --zone-id <zone-id> --hostname api.example.com --tunnel-id <tunnel-id>
npx cftunnel route remove <tunnel-id> --hostname app.example.com
npx cftunnel route add <tunnel-id> --hostname app.example.com --service http://localhost:9000
npx cftunnel route remove <tunnel-id> --hostname app.example.com
npx cftunnel dns list --zone-id <zone-id>
npx cftunnel dns delete <record-id> --zone-id <zone-id>
npx cftunnel tunnel delete <tunnel-id>
import { createClient, createTunnel, addRoute, createDNS } from 'cftunnel';
const client = createClient(); // reads from env vars
const tunnel = await createTunnel(client, { accountId: '...', name: 'my-app' });
await addRoute(client, {
accountId: '...',
tunnelId: tunnel.id,
hostname: 'app.example.com',
service: 'http://localhost:3000',
});
await createDNS(client, {
zoneId: '...',
hostname: 'app.example.com',
tunnelId: tunnel.id,
});
cloudflared running to serve traffic. Creating a tunnel and routes alone is not enough.http_status:404 rule is always appended automatically. Do not add it manually.route set replaces ALL routes. Use route add/route remove for incremental changes.| Flag | Env Var | Description |
|---|---|---|
--api-token | CLOUDFLARE_API_TOKEN | Cloudflare API token (bearer auth) |
--api-key | CLOUDFLARE_API_KEY | Cloudflare API key (requires --api-email) |
--api-email | CLOUDFLARE_EMAIL | Cloudflare account email |
--account-id | CLOUDFLARE_ACCOUNT_ID | Cloudflare account ID |
npm i -g cftunnel -g