cfctl is a governed Cloudflare control plane for macOS and Linux: an
open-source Rust CLI that makes every change to your Cloudflare account
reviewable before it happens, and provable after.
Cloudflare's API will let anything holding a token delete a zone, purge a cache, or rotate a secret in one unreviewed call — and an agent driving that API is one bad inference away from doing it. cfctl puts a hash-bound plan between intent and mutation: reads run freely, writes become a reviewed plan you approve by exact operation ID, and the result is verified against live Cloudflare state rather than assumed from a 200 response.
It has no MCP dependency, accepts natural-language intent through a local agent, and every command emits stable JSON for automation.
- Quickstart — install, first commands, first governed write
- Operator runbook — the full command lifecycle
- Runtime policy — what needs approval, and why
- Security contract — secrets, hashing, invariants
- Architecture — crates, boundaries, trust sequence
- Telemetry control plane — GraphQL, bounded queries, observability, Logpush, and security response
- Agent landing — first-load doctrine for agents
- Contributing — dev setup, proof lane, release lanes
./bootstrap.sh
cfctl version --json
cfctl doctor --jsonbootstrap.sh requires a tracked-clean checkout and proves the installed
binary is the exact HEAD commit. Both doctors must report the PATH entry
resolving to the running executable; a different or missing PATH binary and
drifted agent instructions are unhealthy states, not warnings.
Every mutation follows one path — deterministic resolution, a reviewed hash-bound plan, explicit or policy authority, one Cloudflare boundary, then operation-specific verification:
flowchart TD
I[Intent] --> R["cfctl resolve"]
R --> C[Selected capability]
C --> G["cfctl guide"]
G --> CALL["cfctl call"]
CALL -->|read| EV[Redacted live evidence]
CALL -->|write| P[Fully pinned PlanV2]
P --> POL{Policy engine}
POL -->|narrow safe class| RUN["cfctl plans run"]
POL -->|everything else| APR["cfctl plans approve --yes"]
APR --> RUN
RUN --> B[One Cloudflare boundary]
B --> V[Operation-specific verification]
V --> DONE[Evidence and journal]
cfctl is a local-first, catalog-driven control plane: it separates intent, live reads, durable authority, one Cloudflare boundary, verification, and evidence.
Will this mutate Cloudflare now? Discovery, guides, workspace inspection, and read capabilities do not write Cloudflare. A mutating cfctl call creates a plan; cfctl plans run is the normal write boundary. A token command with --under-policy may plan and run in one invocation only under an explicitly approved standing authority. Agent output, guide output, and approval alone do not mutate Cloudflare.
What grants authority? The deterministic policy engine grants automatic admission only to the narrow safe class. Otherwise authority is either explicit approval of one reviewed operation ID or explicit approval of one bounded standing token policy. A model never grants authority.
What is persisted? Under its managed state root, cfctl persists profile metadata, the live CapabilityV1 catalog and official-doc caches, workspace registrations and imports, plans, approval and admission checkpoints, transaction journals, standing-authority records, locks, and redacted evidence. Credential values remain in the platform secret store or an explicit mode-0600 sink. The source checkout's compat/v1 tree is inert migration evidence, not runtime state or a live catalog.
What happens after a failure or crash? Once consumption or a boundary attempt is durable, cfctl never guesses that replay is safe. Inspect plans status; use plans rectify to reconcile durable receipts and verification without replaying the original Cloudflare mutation.
What should I do next? Run cfctl version --json and both doctors before work; running-build, PATH-build, or managed-instruction drift is unhealthy. Read token permissions only with an explicit account context (keys permissions --account, adding --user only to select user ownership). Nested fixture basenames are skipped during broader workspace scans; fixture directories are opt-in roots and must be registered directly. Then resolve the intent deterministically (cfctl resolve), browse with cfctl catalog search only when exploring, and inspect the selected capability and its capability-specific guide before calling it.
- Orient (
none) — Check running and PATH build identity, local state, credentials, catalog health, and agent integration. - Discover (
none) — Resolve the intent to the catalog-selected capability and adapter; browse the catalog when exploring. - Read (
read) — Inspect exact live Cloudflare state and registered-workspace impact. Durable state: redacted live-read and source-config evidence - Plan (
none) — Bind the request, account, catalog, impact, cost, verification, and compensation contracts. Durable state: canonical pinned PlanV2, compatible PlanV1 journal projection, and PlanPrepared checkpoint - Admit (
none) — Apply policy, bind any explicit approval, acquire locks, and recheck drift. Durable state: approval, standing reservation, and consumption checkpoints - Execute (
write) — Persist the boundary attempt, then cross exactly one catalog-selected adapter boundary. Durable state: boundary-attempt and response checkpoints - Verify (
read) — Run the operation-specific verifier or record why rectification is required. Durable state: sink and verification receipts - Close or rectify (
none) — Close with evidence or reconcile the durable journal; any compensation is a new plan with independent authority. Durable state: terminal plan status and content-addressed evidence
cfctl version --json
cfctl doctor --json
cfctl agents doctor --json
cfctl keys permissions --account <account-id> --json
cfctl keys permissions --user --account <account-id> --json
cfctl guide --topic standing-authority --json
cfctl resolve <intent> --json
cfctl catalog search <intent> --json
cfctl catalog show <capability-id> --json
cfctl guide <capability-id> --json
cfctl call <capability-id> --json
cfctl plans show <operation-id> --json
cfctl plans approve <operation-id> --yes --json
cfctl plans run <operation-id> --json
cfctl plans status <operation-id> --json
cfctl plans rectify <operation-id> --jsoncfctl "<natural-language request>"
cfctl version
cfctl auth login|status|profiles|use|logout|import-api-token|import-global-key
cfctl keys permissions|mint|rotate|revoke|policy
cfctl keys policy create|list|approve|revoke
cfctl catalog sync|search|show|changes|coverage
cfctl resolve "<natural-language intent>"
cfctl call <capability-id> [selectors/body]
cfctl guide <capability-id>
cfctl guide --topic system|standing-authority
cfctl plans show|approve|run|status|resume|rectify|cancel
cfctl policy admission stage|list|show|diff|approve|activate|rollback
cfctl policy cloudflare list|get|diff|plan
cfctl registry scopes list|discover|adopt|remove
cfctl registry sync|status|coverage|list|get|graph|diff|history|export|rebuild
cfctl registry declarations validate|diff|plan
cfctl registry ownership list|get|check
cfctl events sources|status|history|reconcile
cfctl events bridge inspect|prepare|status
cfctl workspace add|remove|discover|graph|audit
cfctl agents install|doctor|sync
cfctl docs search|changes|coverage
cfctl doctor
cfctl update
cfctl migrate v1
Every command has concise human output and stable --json output, shaped by
the public contracts BuildInfoV1, CapabilityV1, CapabilityGuideV1,
GuideTopicDocumentV1, PlanV1, PolicyDecisionV1, AgentActionV1,
EvidenceV1, and ResultEnvelopeV2.
Day-to-day auth is a scoped API token, imported only through stdin — never argv — and pinned to one account:
printf '%s' "$CLOUDFLARE_API_TOKEN" | \
cfctl auth import-api-token --account <account-id> --stdinThe token lives in the platform keyring (Keychain on macOS, Secret Service on
Linux) and falls back to a mode-0600 file store when the keyring is
unavailable; cfctl doctor reports which backend is active.
Two things that commonly bite:
- If a wrapper routes stdin through
cargo, pass a new mode-0600 file with--value-ininstead, so the secret never touches stdin. CFCTL_FORCE_IPV4=1pins outbound calls to an IPv4 source, which an IP-allowlisted token needs when the host default-routes over IPv6.
OAuth with PKCE is available when you have a Cloudflare OAuth client
(--client-id / CFCTL_OAUTH_CLIENT_ID). An emergency global key can be
imported with cfctl auth import-global-key, and is never selected silently.
Reads run immediately and leave redacted evidence:
cfctl call zones-get --query name=example.com --jsonA mutating call writes nothing. It produces a hash-bound plan you review by
exact operation ID:
cfctl guide dns-records-for-a-zone-create-dns-record
cfctl call dns-records-for-a-zone-create-dns-record \
--selector zone_id=<zone-id> \
--body-json '{"type":"TXT","name":"_example","content":"hello"}'
cfctl plans show <operation-id> --json
cfctl plans approve <operation-id> --yes --json
cfctl plans run <operation-id> --jsonPlans expire within 24 hours and any relevant drift invalidates approval. A
hash-chained transaction journal persists every checkpoint from reviewed plan
through close, so a plan consumed before a crash is never replayed — inspect
plans status and reconcile with plans rectify. If you approved something
you no longer want, plans cancel <operation-id> retires it immediately
rather than leaving latent authority to expire.
A narrow safe class of known, scoped, reversible operations runs without separate approval. Deletes, purges, identity and ownership changes, external sends, billing actions, irreversible changes, and anything paid always require it — see runtime policy for the exact contract, and the security contract for per-capability invariants.
Secret outputs never reach stdout, plans, logs, or evidence. They require a new file sink, created mode 0600:
cfctl call cloudflare-tunnel-get-a-cloudflare-tunnel-token \
--selector account_id=<account-id> \
--selector tunnel_id=<tunnel-id> \
--value-out /tmp/tunnel-tokenToken creation is exposed only through the inventory-bound keys mint
workflow, never a direct create call.
Expect to meet the wall early, and read it as the design working. cfctl
catalogs every Cloudflare operation but executes a mutation only when its
contract is fully known — risk, effect, cost, permission, entitlement,
verification, and rollback or explicit irreversibility. Most mutating
operations are blocked on incomplete upstream metadata — the executable core is
a deliberate minority — and cfctl will not fabricate a permission or a price to
close that gap: a guessed one is worse than an honest block. The live ratio is
whatever catalog coverage reports; this file does not restate a number it
cannot keep true.
cfctl catalog coverage --json # what is executable, and what is not
cfctl call workflow.telemetry.audit-account --json # bounded proof preview; blocked/gapped steps have no runnable call
cfctl catalog show <capability-id> # exactly which fields are missingBlocked capabilities stay discoverable and explain themselves. Most of the root cause is upstream and filed in upstream schema gaps.
Registered roots bound all discovery — cfctl never scans outside them:
cfctl workspace add /absolute/repository/root --account <account-id>
cfctl workspace remove /absolute/repository/root
cfctl workspace discover --json
cfctl workspace audit --jsonRemoving a root stops future discovery and removes its account pin while
preserving historical graph and evidence records. Discovery excludes nested
generated, cache, fixture, vendor, and nested-repository paths unless they are
registered directly. It inventories Git repositories even when they carry no Cloudflare
configuration, and links Wrangler TOML/JSON/JSONC, Terraform HCL/JSON, and
Pulumi YAML to catalog targets with current-content, HEAD-content, and exact
worktree-diff hashes, so dirty or unmanaged dependencies stay visible in a
plan.
The registry is a rebuildable local projection, not a replacement authority:
cfctl registry sync --json
cfctl registry coverage --json
cfctl registry list --json
cfctl registry diff --jsonCatalog operations, source configuration, desired declarations under
CFCTL_HOME/config/registry/declarations/, live observations, and evidence
remain distinct. Coverage stays partial whenever a live normalization
provider, permission, or fresh observation is missing. registry rebuild
creates a consistent SQLite backup before reconstructing derived rows.
Admission policy is staged, reviewed, approved, and atomically activated as a separate local policy input. A bundle can tighten the compiled hard safety floor but cannot override ambiguity, incomplete contracts, unknown cost, secret hazards, stale observations, or pinned-state drift:
cfctl policy admission list --jsonNew mutation plans carry a PlanV2 pin set while the PlanV1 body remains readable for compatibility. Historical unconsumed PlanV1 mutations must be replanned before approval or execution.
Events are durable reconciliation triggers, never observed resource truth:
cfctl events sources --json
cfctl events status --json
cfctl events bridge inspect --json
cfctl call events-consume-queue-batch \
--selector account_id=<account-id> --selector queue_id=<queue-id> \
--selector subscription_id=<subscription-or-webhook-id> \
--body-json '{"batch_size":100,"visibility_timeout_ms":60000}' --jsonThe synthetic event-batch capability is the only Queue pull/ack lane. Each
batch is one fully pinned ordinary PlanV2 with explicit approval and the
reviewed USD 0.00016 maximum; raw pull and acknowledgement capabilities remain
blocked. Event evidence and derived reconciliation jobs commit atomically
before exact lease acknowledgement. The inbound Worker under
bridge/event-ingress is managed with Bun (bun install --frozen-lockfile,
bun run check); preparing its manifest does not deploy it.
cfctl agents install --all-detected
cfctl agents doctorNatural language launches one configured agent (CFCTL_AGENT; default
codex, also claude, cursor, gemini). The agent turns intent into a
deterministic cfctl resolve match and governed commands — model output never
grants authority or mutates Cloudflare directly. Quote natural language: a
bare unknown token fails closed with a did-you-mean, so a typo is never an
agent launch. Browser and Computer Use are available only for cataloged
governed_ui capabilities, under the same account binding, redaction,
approval, and evidence rules.
Meaningful operations leave redacted, content-addressed local evidence, with the evidence class distinguishing source config, live reads, plans, applies, post-change verification, agent actions, and local proof. Artifact presence is not verification.
cfctl migrate v1 copies safe desired state and non-secret evidence into
content-addressed imports, skipping secret-shaped paths and never importing
credentials implicitly. Retained v1 data is quarantined under
compat/v1/ as inert migration evidence; the live
catalog is managed under CFCTL_HOME.
Rust 1.97 is pinned. The local proof lane is cargo xtask verify, which also
needs cargo-deny and Gitleaks. Remote CI is intentionally absent. See
CONTRIBUTING.md for setup, the pre-push gate, and the
assembly, signing, and publishing lanes.
Published releases are unsigned by operator decision, with integrity from
SHA256SUMS, reproducible double-builds, SPDX SBOMs, and commit-bound
provenance. Install by direct download plus checksum verification, the
release's Homebrew formula, or a source build.
cfctl.io registration, site publication, publisher-domain verification,
permanent Cloudflare OAuth promotion, and release publication all require
explicit operator action and are never silently performed or claimed.