Skip to content

feat(wrangler): add wrangler tunnel#12492

Draft
thomasgauvin wants to merge 7 commits intocloudflare:mainfrom
thomasgauvin:tgauvin/wrangler-tunnel-commands
Draft

feat(wrangler): add wrangler tunnel#12492
thomasgauvin wants to merge 7 commits intocloudflare:mainfrom
thomasgauvin:tgauvin/wrangler-tunnel-commands

Conversation

@thomasgauvin
Copy link
Contributor

@thomasgauvin thomasgauvin commented Feb 9, 2026

Manage Cloudflare Tunnels

COMMANDS
wrangler tunnel create Create a new Cloudflare Tunnel
wrangler tunnel delete Delete a Cloudflare Tunnel
wrangler tunnel info Display details about a Cloudflare Tunnel
wrangler tunnel list List all Cloudflare Tunnels in your account
wrangler tunnel update Update a Cloudflare Tunnel
wrangler tunnel run [tunnel] Run a Cloudflare Tunnel using cloudflared
wrangler tunnel quick-start Start a free, temporary tunnel without an account (https://try.cloudflare.com)
wrangler tunnel route Configure routing for a Cloudflare Tunnel (DNS hostnames or private IP networks)
wrangler tunnel service Manage cloudflared as a system service
wrangler tunnel cleanup <tunnels..> Remove stale tunnel connections
wrangler tunnel token Fetch the credentials token for an existing tunnel (by name or UUID) that allows to run it

GLOBAL FLAGS
-c, --config Path to Wrangler configuration file [string]
--cwd Run as if Wrangler was started in the specified directory instead of the current working directory [string]
-e, --env Environment to use for operations, and for selecting .env and .dev.vars files [string]
--env-file Path to an .env file to load - can be specified multiple times - values from earlier files are overridden by values in later files [array]
-h, --help Show help [boolean]
-v, --version Show version number [boolean] commands for managing Cloudflare Tunnels
Adds tunnel management commands that align with the cloudflared CLI:

  • CRUD: create, list, info, update, delete
  • Runtime: run (with token/token-file support), quick-start (Try Cloudflare)
  • Routing: route dns, route ip (add/list/delete/get)
  • Operations: cleanup, token (with --cred-file), service install/uninstall
    Includes automatic cloudflared binary download and caching in
    ~/.wrangler/cloudflared/ with SHA256 verification, platform detection,
    and WRANGLER_CLOUDFLARED_PATH override support.

Fixes #[insert GH or internal issue link(s)].

Describe your change...


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because:

A picture of a cute animal (not mandatory, but encouraged)


Open with Devin

@thomasgauvin thomasgauvin requested a review from a team as a code owner February 9, 2026 17:18
@changeset-bot
Copy link

changeset-bot bot commented Feb 9, 2026

🦋 Changeset detected

Latest commit: 41a958f

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR


The `run` and `quick-start` commands automatically download and manage the cloudflared binary, caching it in `~/.wrangler/cloudflared/`. You can override the binary location with the `WRANGLER_CLOUDFLARED_PATH` environment variable.

These commands align with the `cloudflared tunnel` CLI naming conventions.
Copy link
Contributor

@vicb vicb Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huge PR!

Maybe we should start with one/a few commands, do a review and add more later?

Who is the POC on the ANT team for this feature?

These commands align with the cloudflared tunnel CLI naming conventions.

IMO we should rather align with other wrangler commands... or use cloudflared tunnel directly ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe @petebacondarwin is the POC for us. Our Tunnel requirement is that we need to align wrangler commands with cloudflared so that there is no discrepancy between how developers use different Cloudflare CLIs.

Comment on lines 19 to 20
force: {
type: "boolean",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
force: {
type: "boolean",
force: {
alias: "y",
type: "boolean",

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This suggestion will likely break CI as the indentation is wrong. This either needs to be applied manually or have its indentation fixed to match the type property.

export const tunnelDeleteCommand = createCommand({
metadata: {
description: "Delete a Cloudflare Tunnel",
status: "stable",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should always introduce new feature as experimental first

Comment on lines +38 to +40
logger.log(`Deleting tunnel "${args.tunnel}"`);
const tunnelId = await resolveTunnelId(sdk, accountId, args.tunnel);
await deleteTunnel(sdk, accountId, tunnelId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the tunnel does not exist?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API error is passed to the user as is in this case:

✔ Are you sure you want to delete tunnel "player"? This action cannot be undone. … yes
Deleting tunnel "player"

✘ [ERROR] "player" is neither the ID nor the name of any of your tunnels

Copy link
Contributor

@vicb vicb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR Thomas.

I only took a quick look (description + delete) and added a few comments.

@petebacondarwin petebacondarwin marked this pull request as draft February 10, 2026 15:23
@petebacondarwin
Copy link
Contributor

Converting to draft since this PR is to act as a spec for the final commands that are to be landed as part of this feature.


Adds a new set of commands for managing Cloudflare Tunnels directly from Wrangler:

- `wrangler tunnel create <name>` - Create a new Cloudflare Tunnel

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these API operation to work the user usually has to run cloudflared login. How does this work here?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API wrapper.


- `wrangler tunnel token <tunnel>` - Print the tunnel token (or write credentials JSON with `--cred-file`)
- `wrangler tunnel cleanup <tunnels...>` - Remove stale tunnel connections
- `wrangler tunnel service install <tunnel>` / `wrangler tunnel service uninstall` - Install/uninstall cloudflared as a system service

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will also require downloading cloudflared, but it's not mentioned below


- `wrangler tunnel token <tunnel>` - Print the tunnel token (or write credentials JSON with `--cred-file`)
- `wrangler tunnel cleanup <tunnels...>` - Remove stale tunnel connections
- `wrangler tunnel service install <tunnel>` / `wrangler tunnel service uninstall` - Install/uninstall cloudflared as a system service
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this makes sense for wrangler to support. Cloudflared running as a service is more for servers, wrangler is a cli tool for users. This doesn't seem useful

- `wrangler tunnel cleanup <tunnels...>` - Remove stale tunnel connections
- `wrangler tunnel service install <tunnel>` / `wrangler tunnel service uninstall` - Install/uninstall cloudflared as a system service
- `wrangler tunnel route dns <tunnel> <hostname>` - Create a DNS CNAME route to a tunnel
- `wrangler tunnel route ip add <network> <tunnel>` / `list` / `delete` / `get` - Manage private network routes for WARP
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this makes sense for wrangler to support. Wrangler will be mostly used by Workers VPC users. Those do not use ip routes in any capacity

"--credentials-contents",
"SECRET_CREDS",
"--origincert",
"/path/to/cert.pem",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

origin cert is something no longer supported so this can be removed

"run",
"--token",
"SECRET_TOKEN",
"--credentials-contents",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should drive only to use secret_token, and avoid them using credential files. We can fetch the token from the API's


expect(binPath).toContain(expectedDir);

if (process.platform === "win32") {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is win32 the only Windows platform?

import { decodeTunnelTokenToCredentialsFile } from "../tunnel/credentials";

describe("tunnel token credentials decoding", () => {
it("decodes cloudflared-style tunnel token into credentials file shape", () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does wrangler need to know about the tunnel token format?

conns_active_at: tunnel.conns_active_at as string | undefined,
conns_inactive_at: tunnel.conns_inactive_at as string | undefined,
tun_type: tunnel.tun_type as CloudflareTunnelResource["tun_type"],
connections: tunnel.connections as CloudflareTunnelResource["connections"],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the tunnel list endpoint, connections are deprecated and will be removed in the future, so just avoid using it there.

import { resolveTunnelId } from "../client";

/**
* Hostname validation matching cloudflared's implementation.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does wrangler need to do an extra validation? Can't it let cloudflared do the validation and return any error?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh this is not calling cloudflared tunnel route dns. It's calling the API directly.

@@ -0,0 +1,10 @@
import { createNamespace } from "../../core/create-command";

export const tunnelRouteNamespace = createNamespace({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a namespace?

conns_active_at?: string;
conns_inactive_at?: string;
tun_type?:
| "cfd_tunnel"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For wranlger we only need to care about cfd_tunnel.

* Create a new tunnel
*/
export async function createTunnel(
sdk: Cloudflare,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with this code base. What is sdk? Is it a cloudflare API client?

}

async function getExpectedAssetSha256(assetName: string): Promise<string> {
const release = await getGithubRelease(CLOUDFLARED_VERSION);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should get the latest version.

return cached;
}

const url = `https://api.github.com/repos/cloudflare/cloudflared/releases/tags/${version}`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we worried about hitting GitHub API rate limit?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually have a worker called by cloudflared for updates https://github.com/cloudflare/cloudflared/blob/master/cmd/cloudflared/updater/update.go#L29. It takes the OS, arch and current version in query parameters https://github.com/cloudflare/cloudflared/blob/master/cmd/cloudflared/updater/workers_service.go#L64.
This worker uses KV for caching.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes please use this to check for versions, its the same API used by cloudflared

if (!response.ok) {
throw new UserError(
`[cloudflared] Failed to fetch cloudflared release metadata from ${url}\n\n` +
`HTTP ${response.status}: ${response.statusText}`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to return the error message in response body?

logger.log(` You'll get a random *.trycloudflare.com URL to share.`);
logger.log(` The tunnel will stop when you press Ctrl+C.\n`);

// Spawn cloudflared process with automatic binary management

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does automatic binary management mean?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If wrangler downloads cloudflared for the user, it also auto-updates it. Every invocation queries what the latest cloudflared version is, and if the cached version doesn't match the latest, it downloads the new one.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this if we only support quick tunnels.

@ascorbic
Copy link
Contributor

Love it. It would be great if we could support something like wrangler dev --tunnel, to expose your local wrangler dev server via a tunnel.

@petebacondarwin
Copy link
Contributor

Love it. It would be great if we could support something like wrangler dev --tunnel, to expose your local wrangler dev server via a tunnel.

I think that is a nice feature that is sort of orthogonal to this - except that they would both potentially download and run cloudflared.

Additional tunnel tooling:

- `wrangler tunnel token <tunnel>` - Print the tunnel token (or write credentials JSON with `--cred-file`)
- `wrangler tunnel cleanup <tunnels...>` - Remove stale tunnel connections

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This functionality shouldn't be needed as it's a legacy supported command but extremely unlikely to need to be used by current customers.

- `wrangler tunnel create <name>` - Create a new Cloudflare Tunnel
- `wrangler tunnel list` - List all tunnels in your account
- `wrangler tunnel info <tunnel>` - Display details about a specific tunnel
- `wrangler tunnel update <tunnel> --name <new-name>` - Rename a tunnel

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't imagine customers will really leverage this functionality as you can only adjust the tunnel name. Let's omit it for now and only add it if we get some user requests for it.


Additional tunnel tooling:

- `wrangler tunnel token <tunnel>` - Print the tunnel token (or write credentials JSON with `--cred-file`)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would not like this functionality to be provided for two reasons:

  1. We are hoping to remove this functionality in the future and lean more heavily into proper token management instead of allowing customers to read the token after tunnel creation.
  2. We don't want to restrict our future functionality (described in 1.) by then needing to support this command in wrangler for the lifetime of the wrangler-cli. In cloudflared we support a strict 1 year lifetime for existing cloudflared versions to allow us the capability to make backwards incompatible changes in the future.

The recommended way that we should support running tunnels in wrangler should be:

  1. Create a tunnel (via the command)
  2. Save the secret returned in the ~/.cloudflared directory (the same way that cloudflared does)
  3. Running a tunnel then requires either:
    1. A provided token (as manual input) via wrangler tunnel run --token <token>
    2. Fetching the token from ~/.cloudflared directory via wrangler tunnel run <tunnel> (requires looking up the tunnel id to fetch the right secret from the directory)
    3. Providing token file manually via wrangler tunnel run --token-file <token-file>


Additional tunnel tooling:

- `wrangler tunnel token <tunnel>` - Print the tunnel token (or write credentials JSON with `--cred-file`)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--cred-file in wrangler should just be unified to --token-file as there is no reason to rely on the confusing legacy naming that cloudflared uses.

There are two options that should be supported for wrangler tunnel run:

  • --token <token>
  • --token-file <token-file>

// PUT /zones/{zoneTag}/tunnels/{tunnelId}/routes
const result = await fetchResult<DNSRouteResult>(
config,
`/zones/${zoneTag}/tunnels/${tunnelId}/routes`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using this endpoint, please leverage the standard DNS API to create a CNAME route for the tunnel.

https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/create/


logger.log(`Deleting tunnel "${args.tunnel}"`);
const tunnelId = await resolveTunnelId(sdk, accountId, args.tunnel);
await deleteTunnel(sdk, accountId, tunnelId);
Copy link

@DevinCarr DevinCarr Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are resources (hostname routes, IP routes) attached to the tunnel, a user will be unable to delete it. Not sure what this error condition will look like in that case.

Copy link

@nikitacano nikitacano Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It passes the error onto the user as is:

✔ Are you sure you want to delete tunnel "play"? This action cannot be undone. … yes
Deleting tunnel "play"

✘ [ERROR] A request to the Cloudflare API failed.

  Cannot delete tunnel because it has Warp routing configured. You can delete the route with
  `cloudflared tunnel route ip delete`. [code: 1023]
  
  If you think this is a bug, please open an issue at:
  https://github.com/cloudflare/workers-sdk/issues/new/choose

): Promise<CloudflareTunnelResource> {
return withTunnelPermissionCheck(async () => {
const response = (await sdk.zeroTrust.tunnels.cloudflared.create({
account_id: accountId,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every tunnel through this tool should only be creating remotely managed tunnels which requires another field: config_src: "cloudflare". Locally managed tunnels are not the direction that we want to support for this use-case. Future improvements to ingress rule APIs will also go in this direction.

https://developers.cloudflare.com/api/resources/zero_trust/subresources/tunnels/subresources/cloudflared/methods/create/

import type { ChildProcess } from "node:child_process";

// cloudflared version to download
export const CLOUDFLARED_VERSION = "2026.1.2";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are pinning a cloudflared version to a wrangler version? Do we instead want to keep the latest version of cloudflared?

errorMessage += ` - You're missing required system libraries\n`;

if (process.platform === "linux") {
errorMessage += `\nOn Linux, make sure you have the required dependencies:\n`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the error for? When does this happen?

` - The version ${CLOUDFLARED_VERSION} doesn't exist\n` +
` - GitHub is temporarily unavailable\n` +
` - You're being rate limited\n\n` +
`You can manually download cloudflared from:\n` +

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's point users to normal client download mechanisms instead of github if we can help it.

`Spawning cloudflared: ${binPath} ${redactCloudflaredArgsForLogging(args).join(" ")}`
);

const cloudflared = spawn(binPath, args, {
Copy link

@DevinCarr DevinCarr Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have the capability to output cloudflared logs as json (--output json), do we want to leverage that here at all, or just dump the standard log lines?

}

const knownPlatforms: Record<string, BinarySpec> = {
"darwin arm64 LE": {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the endianness needed?

thomasgauvin and others added 4 commits February 17, 2026 15:08
Manage Cloudflare Tunnels

COMMANDS
  wrangler tunnel create <name>        Create a new Cloudflare Tunnel
  wrangler tunnel delete <tunnel>      Delete a Cloudflare Tunnel
  wrangler tunnel info <tunnel>        Display details about a Cloudflare Tunnel
  wrangler tunnel list                 List all Cloudflare Tunnels in your account
  wrangler tunnel update <tunnel>      Update a Cloudflare Tunnel
  wrangler tunnel run [tunnel]         Run a Cloudflare Tunnel using cloudflared
  wrangler tunnel quick-start <url>    Start a free, temporary tunnel without an account (https://try.cloudflare.com)
  wrangler tunnel route                Configure routing for a Cloudflare Tunnel (DNS hostnames or private IP networks)
  wrangler tunnel service              Manage cloudflared as a system service
  wrangler tunnel cleanup <tunnels..>  Remove stale tunnel connections
  wrangler tunnel token <tunnel>       Fetch the credentials token for an existing tunnel (by name or UUID) that allows to run it

GLOBAL FLAGS
  -c, --config    Path to Wrangler configuration file  [string]
      --cwd       Run as if Wrangler was started in the specified directory instead of the current working directory  [string]
  -e, --env       Environment to use for operations, and for selecting .env and .dev.vars files  [string]
      --env-file  Path to an .env file to load - can be specified multiple times - values from earlier files are overridden by values in later files  [array]
  -h, --help      Show help  [boolean]
  -v, --version   Show version number  [boolean] commands for managing Cloudflare Tunnels
Adds tunnel management commands that align with the cloudflared CLI:
- CRUD: create, list, info, update, delete
- Runtime: run (with token/token-file support), quick-start (Try Cloudflare)
- Routing: route dns, route ip (add/list/delete/get)
- Operations: cleanup, token (with --cred-file), service install/uninstall
Includes automatic cloudflared binary download and caching in
~/.wrangler/cloudflared/ with SHA256 verification, platform detection,
and WRANGLER_CLOUDFLARED_PATH override support.
Remove commands not needed for remotely managed tunnels: route (dns/ip),
service, cleanup, token, update, and credentials. Enforce token-only auth
with config_src:'cloudflare' on create, strip deprecated tunnel types
and connections field from the API client.

Rewrite cloudflared binary management to use Cloudflare's update worker
(update.argotunnel.com) instead of GitHub API, with a GitHub release URL
fallback for platforms the update worker doesn't cover (e.g. darwin/arm64).
Prompt users before downloading cloudflared, warn when PATH-installed
binary is outdated, and pass tunnel tokens via TUNNEL_TOKEN env var
instead of --token-file for broader compatibility and security.

Mark all tunnel commands as experimental and add subway emoji to the
namespace description.
@nikitacano nikitacano force-pushed the tgauvin/wrangler-tunnel-commands branch from 0007853 to 0288b26 Compare February 17, 2026 15:12
@github-project-automation github-project-automation bot moved this to Untriaged in workers-sdk Feb 17, 2026
@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 17, 2026

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@12492

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@12492

miniflare

npm i https://pkg.pr.new/miniflare@12492

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@12492

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@12492

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@12492

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@12492

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@12492

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@12492

wrangler

npm i https://pkg.pr.new/wrangler@12492

commit: 41a958f

- Remove expect from vitest imports in all tunnel test files
- Add expect parameter to all test callbacks per workers-sdk/no-vitest-import-expect rule
- Fix type annotations to avoid explicit any usage
- Use type-only import for CloudflaredModule to avoid forbidden import() type annotation
- Remove duplicate test in tunnel-resolve.test.ts
- Fix handler access by casting tunnelRunCommand to any
- Add config_src property to mockTunnelCreate return type
- Remove duplicate CloudflaredModule import
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

10 participants

Comments