Lightweight Cloudflare management CLI built with Laravel Zero and Saloon.
Download cf from Releases, then:
chmod +x cf
sudo mv cf /usr/local/bin/cfRequires PHP 8.3+ (zlib). Export CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID before running commands.
composer require conduit-ui/cloudflare
# or globally:
composer global require conduit-ui/cloudflaregit clone https://github.com/conduit-ui/cloudflare.git
cd cloudflare
composer installBuild a local PHAR after composer install with composer build (or php cf app:build cf). Artifact: builds/cf.
Run setup first to validate credentials and write .env:
./cf setup
# or non-interactive (agents/CI):
./cf setup --token=cf_xxx --account-id=abc123 --non-interactive
./cf setup --token=cf_xxx --account-id=abc123 --non-interactive --jsonYou can also copy .env.example and edit by hand:
cp .env.example .env
# CLOUDFLARE_API_TOKEN=your_token_here
# CLOUDFLARE_ACCOUNT_ID=your_account_idGet credentials from:
- API Token: https://dash.cloudflare.com/profile/api-tokens
- Account ID: Right sidebar on any zone page
| Command | Required Permission |
|---|---|
zones |
Zone:Read |
dns:* |
DNS:Edit |
tunnel:* |
Cloudflare Tunnel:Edit |
Pass --json for a stable envelope on stdout: {"ok":true,"data":...} on success, {"ok":false,"error":"..."} on failure (exit 0 / 1). See docs/agent-output.md.
./cf setup # Interactive auth onboarding
./cf setup --non-interactive --token=... --account-id=..../cf zones # List all zones
./cf zones --json # Agent JSON envelope./cf dns:list <zone> # List records (zone ID or domain)
./cf dns:list jordanpartridge.us --type=A # Filter by type
./cf dns:create <zone> A api 1.2.3.4 # Create A record
./cf dns:create <zone> CNAME www example.com --proxied
./cf dns:update <zone> <id> A api 5.6.7.8 # Update record (PUT)
./cf dns:update <zone> <id> A api 5.6.7.8 --proxied --ttl=300 --json
./cf dns:delete <zone> <id> # Delete record (prompts)
./cf dns:delete <zone> <id> --force --json # Skip confirmation./cf tunnel:list # List all tunnels
./cf tunnel:get <id> # Get tunnel details
./cf tunnel:get <id> --json
./cf tunnel:create <name> # Create tunnel (prints token/credentials)
./cf tunnel:expose <name> [hostname] [url] # Create + route + cloudflared steps
./cf tunnel:delete <id> # Delete tunnelOne guided flow: create a tunnel, print the connector token, and (when a hostname is given) attempt remote ingress + DNS CNAME via the API. Falls back to exact cloudflared commands when the API cannot route.
# Default local service: http://localhost:8000
./cf tunnel:expose my-app
# Hostname + custom local URL
./cf tunnel:expose my-app app.example.com http://localhost:3000
# Full create payload (token, credentials_file, routing results)
./cf tunnel:expose my-app app.example.com --jsonThen run cloudflared with the printed token:
cloudflared tunnel run --token <token-from-output>If DNS was not created via API:
cloudflared tunnel route dns my-app app.example.comtunnel:create masks secrets in the summary table and prints full token / credentials once. Use --json for the agent envelope ({"ok":true,"data":...}).
app/
├── Commands/ # CLI commands
├── Integrations/Cloudflare/
│ ├── CloudflareConnector.php # Saloon connector
│ ├── Resources/ # API resource classes
│ └── Requests/ # Individual API requests
PR quality is quality-runners (Pint, Pest, PHPStan, Rector on the clone host). Until that runner is subscribed to this repo, GitHub Actions still runs Pint --test and Pest as a residual. Local ./bin/ci is the full set.
Locally:
composer install
./bin/ciMIT