Claude
Skills
Sign in
Back

cloudflare-tunnel-publish

Included with Lifetime
$97 forever

Publish a local service to a user-owned custom domain via Cloudflare Tunnel. Use when the user wants their own domain instead of community.iamstarchild.com (e.g. app.mydomain.com, fix SSL 521 on a custom domain).

Cloud & DevOpsscripts

What this skill does


## What this skill does

Turns a service running on a local port (default: a Starchild preview, but works for any HTTP service) into something the world can reach at `app.userdomain.com`, using **Cloudflare Tunnel**. No public IP required, no inbound ports opened, free SSL.

**Two roles in the flow:**

- **User does manually** (must, can't be automated): create Cloudflare account, buy/transfer domain to Cloudflare, create API Token.
- **Agent does automatically** (this skill): verify token, pick zone, create tunnel, configure ingress, create DNS, install + start `cloudflared`, verify the public URL works.

## Audience assumption

Treat the user as a **beginner**. They may have never used Cloudflare. Walk them through one micro-step at a time, wait for confirmation, then move on. Do NOT dump the whole 10-step plan and disappear.

## Workflow

### Phase 0 — Set the stage (1 message)

Tell the user in plain language what's about to happen, in 4 phases:

1. They register a Cloudflare account + add a domain (manual, ~5 min)
2. They create an API Token and give it to you securely (manual, ~2 min)
3. You build the tunnel + DNS + start it (automatic, ~1 min)
4. You test the URL together (automatic)

Ask: **"Do you already have a domain on Cloudflare, or do we need to start from scratch?"** Branch on the answer.

### Phase 1 — Get the user a domain on Cloudflare

If they don't have one yet:

- Direct them to https://dash.cloudflare.com/sign-up to register
- Then https://dash.cloudflare.com/?to=/:account/domains to buy a domain (Cloudflare sells `.com / .net / .org / .io / .dev / .app` etc. at registry cost), OR add an existing domain and change nameservers
- Wait for them to confirm "domain is active in Cloudflare" before proceeding

Beginner hint to share: "When the domain shows status **Active** in your Cloudflare dashboard, we're good to continue."

If they already have one: skip to Phase 2.

### Phase 2 — Create the API Token

Send them this exact link (it pre-fills the right permissions when possible, and the user can also build it manually):

> https://dash.cloudflare.com/profile/api-tokens → **Create Token** → **Create Custom Token**

Required permissions (tell them to add these three):

- **Account** → **Cloudflare Tunnel** → **Edit**
- **Zone** → **DNS** → **Edit**
- **Zone** → **Zone** → **Read**

Account Resources: their account. Zone Resources: **Include All zones** (or specifically the domain). TTL: leave default.

After they click **Continue to summary** → **Create Token** → Cloudflare shows the token **once**. Tell them: **do not paste it in chat**.

### Phase 3 — Receive the token securely

Call `request_env_input` with:

```
env_vars=[{"key": "CLOUDFLARE_API_TOKEN", "label": "Cloudflare API Token", "required": true}]
reason="Used to create the tunnel and DNS record on your domain. Stored locally in workspace/.env, never echoed in chat."
```

Wait for the user to submit it via the secure popup. **Do not retry-loop** if they don't submit immediately — just wait.

### Phase 4 — Verify token + pick the zone

Run `python3 skills/cloudflare-tunnel-publish/scripts/verify.py`. It prints:
- Token validity
- The user's account_id (saves to `workspace/.cf_state.json`)
- All zones (domains) on the account

If multiple zones, ask the user which domain to use. Save `zone_id` and `zone_name` to state.

### Phase 5 — Decide what to publish

Ask the user two things:

1. **Subdomain** (e.g., `app`, `demo`, `www`) → final hostname will be `<sub>.<zone_name>`. Apex domain (`@`) is also allowed.
2. **Local port** (e.g., `8080`, `3000`). If they say "my Starchild preview", run `cat /data/previews.json 2>/dev/null` to look up an existing preview's port; otherwise ask explicitly.

Default service URL: `http://localhost:<port>`.

### Phase 6 — Build the tunnel (automated)

Run `python3 skills/cloudflare-tunnel-publish/scripts/setup.py --hostname <full_hostname> --port <port>`.

The script does, in order:
1. Create a remotely-managed tunnel (`config_src: "cloudflare"`) named `starchild-<hostname>`
2. Fetch the tunnel **run token** (a long base64 string used to start `cloudflared`)
3. PUT the ingress configuration: `<hostname>` → `http://localhost:<port>`, fallback `404`
4. Create a CNAME DNS record: `<hostname>` → `<tunnel_id>.cfargotunnel.com`, **proxied = true**
5. Save tunnel_id + run_token + hostname to `workspace/.cf_state.json`

If a tunnel with the same name exists, reuse it instead of erroring.

### Phase 7 — Start cloudflared

Run `bash skills/cloudflare-tunnel-publish/scripts/run_tunnel.sh`. It:
- Downloads the `cloudflared` binary to `workspace/bin/cloudflared` if missing
- Reads run_token from `workspace/.cf_state.json`
- Launches `cloudflared tunnel run --token <TOKEN>` in background via `bash(background=true)`
- Returns the bash session_id so we can poll/log later

Save the session_id to state. Tell the user the tunnel daemon is running.

### Phase 8 — Verify

⚠️ **Do not use the container's `curl https://<hostname>` directly** — the container's resolver caches stale NXDOMAIN for new domains and will lie to you. Always verify via DoH:

```bash
curl -sS "https://dns.google/resolve?name=<hostname>&type=A" | python3 -m json.tool
```

Three possible outcomes:

1. **`Status: 0` + IPs in `Answer`** → live. Now `curl -I https://<hostname>` should return 200/301/302. Show the user their URL. 🎉
2. **`Status: 3` (NXDOMAIN) + Authority = TLD registry NS** (e.g. `ns.trs-dns.com`) → **TLD registry hasn't propagated the new domain yet.** Tell the user: configuration is 100% done, wait 30–60 min (newly registered domains can take up to 24 h), then retry. Don't keep polling — let them check on their own device.
3. **Tunnel logs show errors** (check `bash_process(action='log', session_id=...)`) → real config bug. Common culprits: ingress not pointing at the right port, local service not running, wrong CNAME target.

## Decision rules

- **User says "my service is on my laptop, not in Starchild"** → exact same flow, but Phase 7 must run on their laptop, not in this container. Give them the equivalent install command for their OS:
  - macOS: `brew install cloudflared && cloudflared tunnel run --token <TOKEN>`
  - Linux/Windows: link to https://github.com/cloudflare/cloudflared/releases/latest
  Send the run_token via `request_env_input` if needed, or just print it once and tell them to copy it (it's safe to share with their own machine, but never paste back to chat).

- **User wants multiple subdomains** → reuse the same tunnel; PUT a new ingress config that lists all hostnames; create one CNAME per hostname.

- **User wants to remove it** → run `python3 skills/cloudflare-tunnel-publish/scripts/teardown.py` (deletes DNS + tunnel + kills the local cloudflared process).

## Gotchas (⚠️ all confirmed in real runs)

### Token / API
- **`Cloudflare-Tunnel:Edit` does NOT grant `/accounts` listing.** Calling `GET /accounts` returns an empty list even with a valid token. **Solution:** derive `account_id` from any zone's embedded `account.id` field — `verify.py` already does this. Do NOT add `Account:Account Settings:Read` just to fix it; the zone trick is cleaner.
- The "run token" from `GET /accounts/{id}/cfd_tunnel/{tunnel_id}/token` is what `cloudflared tunnel run --token` consumes. Do not confuse with:
  - **tunnel secret** — only relevant for legacy locally-managed tunnels (we don't use)
  - **API Token** — used to call api.cloudflare.com

### Tunnel / Ingress
- The CNAME target must be `<tunnel_id>.cfargotunnel.com`, NOT the tunnel name.
- Remotely-managed tunnel (`config_src: "cloudflare"`) routes via the API config endpoint, NOT a local `config.yml`. Do not generate one.
- Creating a tunnel via API requires a `tunnel_secret` field (32 random bytes, base64) even for `config_src=cloudflare`. `setup.py` generates one automatically.

### Universal SSL provisioning lag — the OTHER big trap
After DNS propagates, the user may still hit `ERR_SSL_VERSION_OR_CIPHER_MISMATCH` in 

Related in Cloud & DevOps