Cloudflare just launched cf, a new command-line interface meant to eventually replace Wrangler as the front door to Cloudflare’s platform. This blog’s deploy pipeline has run on plain Wrangler since day one, so it made a good test subject. This post covers what cf actually is, how the migration went, and where it genuinely behaves differently from Wrangler — with real output, not marketing copy.

What cf is

Wrangler covers roughly 280 commands, almost all of them Workers-related. cf is generated straight from Cloudflare’s public OpenAPI schema and covers the entire product surface — something like 3,000 operations across DNS, R2, D1, Zero Trust, AI Gateway, Access, Queues, Load Balancers, and everything else in the dashboard. One binary instead of “Wrangler for Workers, dashboard clicks for everything else.”

It’s explicitly built for two audiences at once: humans typing commands, and AI agents driving it programmatically. That second part shows up everywhere once you start using it.

Installing it

npm install -g cf

Requires Node 22+. Then authenticate:

cf auth login

This opens a browser for OAuth (device-code flow also works for headless machines). cf auth whoami confirms the session:

$ cf auth whoami
{
  "authenticated": true,
  "authSource": "OAuth token from ~/.config/cloudflare/config/default.json",
  "tokenValid": true,
  "email": "you@example.com",
  "accounts": [ { "id": "...", "name": "your account" } ],
  "scopes": [ "..." ]
}

Credential resolution order, per the tool’s own docs: a CLOUDFLARE_API_TOKEN environment variable first, then the OAuth profile. That order matters later, for CI.

Discovery is the actual headline feature

Run cf --help and the first thing printed isn’t the command list — it’s an instruction block aimed at agents:

=== STOP: AGENT COMMAND DISCOVERY ===
AGENTS: Do not explore commands by chaining nested --help calls.
Your first port of call and the best way to discover commands is:
  cf cli search "<describe the task you want to accomplish>"
It returns five compact JSON matches with short descriptions.
=== END AGENT COMMAND DISCOVERY ===

With ~3,000 operations, tab-completing your way through nested --help trees doesn’t scale. So cf ships a search command instead:

$ cf cli search "list dns records for a zone"
[
  { "command": "cf dns records list", "summary": "List DNS Records" },
  { "command": "cf dns records scan-list", "summary": "List Scanned DNS Records" },
  { "command": "cf dns records scan-review", "summary": "Review Scanned DNS Records" },
  { "command": "cf dns records scan", "summary": "Scan DNS Records" },
  { "command": "cf dns records import", "summary": "Import DNS Records" }
]

Natural-language query in, five ranked commands out. This is the practical difference between “large CLI” and “usable large CLI.” I also asked it to list zones on the account and got clean, complete JSON back — no flags needed to opt into machine-readable output, because JSON is the default:

$ cf zones list
[
  { "id": "...", "name": "hicke.se", "status": "active", "paused": false, ... }
]

There’s also cf schema <command>, which swaps in the raw API request shape for any discovered command — useful when you want to know exactly what’s being sent before you run something with write access.

Migrating the config

The blog’s Cloudflare config before this was a three-line wrangler.jsonc:

{
  "name": "blog",
  "compatibility_date": "2026-05-01",
  "assets": { "directory": "public" }
}

cf replaces .jsonc/.toml config with typed TypeScript, and ships a migration command for exactly this:

cf migrate wrangler.jsonc --dry-run
Using the Wrangler bundler because @cloudflare/vite-plugin is not declared.
Would update 4 file(s):
├─ cloudflare.config.ts
├─ wrangler.config.ts
├─ package.json
└─ package-lock.json

✔ Migration preview complete.

One catch: the codemod needs a local Wrangler install ≥4.100.0 (the version that exports wrangler/experimental-config). This project had none — deploys ran through wrangler-action in CI, with no package.json in the repo at all — so npm install --save-dev wrangler came first. After that, the real run:

cf migrate wrangler.jsonc

produced two files where there used to be one:

// cloudflare.config.ts
import { defineConfig } from "cf/config";

export default defineConfig({
  worker: {
    name: "blog",
    compatibilityDate: "2026-05-01",
  },
});
// wrangler.config.ts
import { defineWranglerConfig } from "wrangler/experimental-config";

export default defineWranglerConfig({
  types: { generate: false },
  assetsDirectory: "public",
});

Worker identity and platform settings live in cloudflare.config.ts; the parts that are genuinely Wrangler’s concern (asset directory, bindings) live in wrangler.config.ts, which cf delegates to under the hood. Nothing about the actual build changed — cf build still just runs Hugo’s output through Wrangler’s asset bundler. cf is a layer on top, not a replacement bundler.

The gotcha: plain Wrangler can’t read the new config

This is the part worth knowing before you migrate a CI pipeline. I tested it directly: with only cloudflare.config.ts + wrangler.config.ts present, running bare wrangler deploy doesn’t pick them up at all — it falls back to an interactive “no config found, want me to create one?” wizard. The split TypeScript config is cf-native; Wrangler itself still wants a .jsonc/.toml, or at minimum a single wrangler.config.ts it produced itself.

Practically: if your CI still shells out to wrangler deploy directly, keep the old wrangler.jsonc around after migrating — cf and Wrangler will happily coexist reading their respective files. This blog’s CI switched over to cf deploy in the same change, so the old file eventually came out entirely (see below).

Updating CI

The original GitHub Actions step used cloudflare/wrangler-action:

- name: Deploy
  uses: cloudflare/wrangler-action@...
  with:
    apiToken: ${{ secrets.CF_API_TOKEN }}
    accountId: ${{ secrets.CF_ACCOUNT_ID }}

There’s no cf-action yet, so this became a plain npm install + CLI invocation, relying on the CLOUDFLARE_API_TOKEN env-var precedence mentioned earlier:

- name: Setup Node
  uses: actions/setup-node@...
  with:
    node-version: 22

- name: Install dependencies
  run: npm ci

- name: Deploy
  run: npx cf deploy
  env:
    CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
    CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}

Same two secrets as before, just renamed to the env vars Wrangler (and by extension cf) actually reads. cf deploy builds and uploads in one step, delegating the build to Wrangler exactly like cf build does locally:

├  Build
│  Delegating to Wrangler
◆  Build complete
├  Deploy
│  ✨ Read 104 files from the assets directory
│  Total Upload: 0.00 KiB / gzip: 0.02 KiB
◆  Deploy complete

First push after the change: green run, site live, ~20 seconds end to end — about the same as the old Wrangler-action step.

What’s actually different, concretely

  • Surface area. Wrangler is Workers-only. cf reaches DNS, Zero Trust, R2, D1, Load Balancers, AI Gateway — the whole dashboard — from one auth session and one binary.
  • Discovery over memorization. cf cli search "<task>" beats knowing (or grepping for) the exact subcommand name across 3,000 operations. This is the single biggest quality-of-life change, agent or not.
  • JSON by default. Every command is scriptable output-first; no --json flag hunting.
  • Typed config. cloudflare.config.ts gets LSP autocomplete and compile-time errors instead of silently-wrong TOML keys.
  • Low-risk adoption. cf build/cf deploy delegate to Wrangler’s actual bundler rather than reimplementing it, and cf migrate is non-destructive by default (--dry-run first, real run needs an explicit clean worktree or --force). Old and new config can run side by side during a transition.
  • Still beta. v1.0.0-beta.5 as of this migration — no official GitHub Action yet, cf schema doesn’t cover every command, and the API surface “changes with the pinned OpenAPI release” per its own README.

For a single-Worker static blog, the CLI’s raw scope was overkill — but the search-driven discovery and the fact that migration didn’t require touching the actual deploy logic made it a low-effort swap. If you’re managing more than Workers across a Cloudflare account, that scope stops being overkill and starts being the whole point.