Tunnel providers
Provider shapes, URL sources, and ownership
tunnels[] is restart-required (/reference/typeclaw-json). The full schema:
type Tunnel = {
name: string
provider: 'cloudflare-quick' | 'cloudflare-named' | 'external'
for: { kind: 'channel'; name: string } | { kind: 'manual' }
upstreamPort?: number // required when `for.kind === 'manual'` AND provider !== 'cloudflare-named'
externalUrl?: string // required when `provider === 'external'`
hostname?: string // required when `provider === 'cloudflare-named'`; must be https://
tokenEnv?: string // required when `provider === 'cloudflare-named'`; env var name in .env
}Providers
| Provider | URL source | Subprocess | Stability |
|---|---|---|---|
cloudflare-quick | parsed from cloudflared stderr at runtime | cloudflared tunnel --url … | rotates every cloudflared restart |
cloudflare-named | static, from hostname in config | cloudflared tunnel --no-autoupdate run --token … | as stable as the dashboard hostname |
external | static, from externalUrl in config | none | as stable as the URL you provided |
The schema rejects unknown provider names so typeclaw start fails fast before tearing down a working container.
cloudflare-named setup
A cloudflare-named tunnel is a "remotely-managed" tunnel (Cloudflare docs). The user creates and configures it in the Cloudflare Zero Trust dashboard; typeclaw only runs cloudflared with the dashboard-issued token.
Required dashboard steps:
- Create a tunnel.
Networks → Tunnels → Create a tunnel → Cloudflared. Name it, install method = anything (typeclaw doesn't use the installer; just the token). Copy the token shown on the install screen. - Add a Public Hostname. On the new tunnel's page,
Public Hostnametab →Add a public hostname. Pick a subdomain + a domain from your account's zones, service typeHTTP, URLlocalhost:<port>where<port>is the in-container upstream.
A tunnel without a Public Hostname registers but routes nothing. curl against the configured hostname returns Cloudflare error 530 or 1033. typeclaw cannot detect this — the tunnel shows healthy.
Required typeclaw steps:
-
Put the token in
.env. Pick a var name (convention:CLOUDFLARE_TUNNEL_TOKEN):echo 'CLOUDFLARE_TUNNEL_TOKEN=eyJhIjoi…' >> .env -
Add the tunnel to
typeclaw.jsonviatypeclaw tunnel addor by hand:{ "tunnels": [ { "name": "github-webhook", "provider": "cloudflare-named", "for": { "kind": "channel", "name": "github" }, "hostname": "https://agent.example.com", "tokenEnv": "CLOUDFLARE_TUNNEL_TOKEN", }, ], } -
typeclaw restartso the Dockerfile is regenerated with thecloudflaredlayer and the tunnel manager picks up the new entry.
Drift between dashboard and config
hostname in typeclaw.json is informational — cloudflared learns the actual hostname→upstream mapping from the dashboard. If the user changes the Public Hostname in the dashboard without updating tunnels[].hostname, traffic stops flowing but typeclaw still reports the stale URL on tunnel-url-changed events. There is no programmatic detection; typeclaw deliberately does not poll Cloudflare's API.
Missing token
If process.env[tokenEnv] is unset or empty when the manager starts, the named provider transitions to permanently-failed with a detail naming the missing env var. cloudflared is never spawned. Fix .env and typeclaw restart.
for discriminator
for | Owned by | Lifecycle |
|---|---|---|
{ kind: 'channel', name: '<adapter>' } | typeclaw channel add + typeclaw tunnel set <name> | adapter subscribes to URL changes; re-registers its webhook on rotation |
{ kind: 'manual' } | typeclaw tunnel add / set / remove | adapter-agnostic; you point --upstream-port at the in-container server |
typeclaw tunnel remove refuses to delete a channel-owned tunnel — use typeclaw tunnel set <name> to change its provider/URL in place. To detach the tunnel from its channel entirely, hand-edit typeclaw.json to remove both the channel block and the tunnel entry.
URL change notifications
Tunnel URL changes propagate over the in-process message stream as a broadcast event with payload:
type TunnelUrlChangedPayload = {
kind: 'tunnel-url-changed'
tunnelName: string
url: string
for: TunnelFor
rotatedAt: string // ISO timestamp
}Channel adapters subscribe via this broadcast, NOT by polling the tunnel manager. Plugin-contributed adapters with webhook needs should subscribe via the same broadcast filter.
Upstream-port pitfall
Some services pair a host-facing compatibility proxy with a separate in-container server (the bundled agent-browser plugin is the canonical example: /tmp/typeclaw-agent-browser-proxy-port is the proxy, /tmp/typeclaw-agent-browser-upstream-port is the actual dashboard server). The tunnel's upstreamPort must name the in-container server, not the proxy. Tunneling the proxy port silently double-hops.
The rule generalizes: any service that pairs a host-side proxy/forwarder with a container-side server publishes both ports separately, and upstreamPort always names the container-side one.