TypeClawTypeClaw
Reference

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

ProviderURL sourceSubprocessStability
cloudflare-quickparsed from cloudflared stderr at runtimecloudflared tunnel --url …rotates every cloudflared restart
cloudflare-namedstatic, from hostname in configcloudflared tunnel --no-autoupdate run --token …as stable as the dashboard hostname
externalstatic, from externalUrl in confignoneas 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:

  1. 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.
  2. Add a Public Hostname. On the new tunnel's page, Public Hostname tab → Add a public hostname. Pick a subdomain + a domain from your account's zones, service type HTTP, URL localhost:<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:

  1. Put the token in .env. Pick a var name (convention: CLOUDFLARE_TUNNEL_TOKEN):

    echo 'CLOUDFLARE_TUNNEL_TOKEN=eyJhIjoi…' >> .env
  2. Add the tunnel to typeclaw.json via typeclaw tunnel add or by hand:

    {
      "tunnels": [
        {
          "name": "github-webhook",
          "provider": "cloudflare-named",
          "for": { "kind": "channel", "name": "github" },
          "hostname": "https://agent.example.com",
          "tokenEnv": "CLOUDFLARE_TUNNEL_TOKEN",
        },
      ],
    }
  3. typeclaw restart so the Dockerfile is regenerated with the cloudflared layer 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

forOwned byLifecycle
{ 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 / removeadapter-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.

On this page