cf-cli Code Review

Bun/TypeScript Cloudflare CLI — 5-Agent Multi-Perspective Analysis • 2026-03-01

Codebase
~34K lines TypeScript
Critical Issues
8
Recommendations
18
Observations
12
Reviewers
5/5

Agent Status

Gemini
gemini-2.5-pro
Completed
26 findings
Claude
claude-opus-4-6
Completed
18 findings
Ocasia
qwen3.5:397b
Completed
14 findings
Rex
devstral-2:123b
Completed
5 findings
Phil
qwen3-coder:480b
Completed
5 findings

Overall Assessment

cf-cli is a well-engineered, production-quality CLI tool. All 5 reviewers independently praised the architecture: clean separation of concerns, typed Context injection, atomic config writes with correct permissions, credential redaction in verbose mode, TTY-safe prompts, and a zero-dependency argument parser. The code shows serious engineering judgment throughout. Critical issues are concentrated in three areas: security (secret handling, input validation), correctness (retry logic, pagination bounds), and robustness (error swallowing, race conditions). All are fixable without architectural changes.

Critical Issues (Must Fix)

1. Secret value accepted as CLI argument
Critical 2-Agent Consensus
secrets-store/secrets/put.ts • --value flag
The --value <secret-value> flag passes plaintext secrets as command-line arguments. On every Unix system, CLI args are visible in /proc/[pid]/cmdline, ps aux, audit logs, and shell history files. This is the single most important security issue.
Suggested Fix
Accept secrets via stdin, environment variable (CF_SECRET_VALUE), or file path (--value-file). Never as a CLI argument.
// Option A: stdin
const value = await readStdin();
// Option B: file
const value = await Bun.file(getStringFlag(flags, "value-file")).text();
// Option C: env var
const value = process.env["CF_SECRET_VALUE"];
Flagged by: Gemini, Claude
2. Retry-After header ignored on 429 responses
Critical 3-Agent Consensus
client.ts • request() retry logic
When the API returns a 429, the code uses fixed exponential backoff (1s, 2s, 4s) and ignores the Retry-After header. This causes either re-banning (too soon) or unnecessary waiting (too long). Cloudflare's API specifies this header.
Suggested Fix
if (response.status === 429 && attempt < MAX_RETRIES) {
  const retryAfter = response.headers.get("Retry-After");
  const waitMs = retryAfter
    ? parseFloat(retryAfter) * 1000
    : (RETRY_DELAYS[attempt - 1] ?? 4000);
  await sleep(waitMs);
  continue;
}
Flagged by: Gemini, Claude, Ocasia
3. Unbounded pagination loop
Critical 2-Agent Consensus
client.ts • fetchAll()
fetchAll loops while (true) with no maximum page guard. A malformed or adversarial result_info (or cycling cursors) will loop indefinitely, exhausting rate-limit quota and memory.
Suggested Fix
const MAX_PAGES = 1000;
while (page <= MAX_PAGES) {
  // ... existing logic ...
}
if (page > MAX_PAGES) {
  throw new Error(`fetchAll: exceeded ${MAX_PAGES} page limit for ${path}.`);
}
Flagged by: Gemini, Claude
4. Inconsistent URL path encoding
Critical 2-Agent Consensus
Multiple command files • URL construction
Some path segments use encodeURIComponent, others don't. Inconsistency creates maintenance risk and a latent injection surface. KV put also manually builds query strings instead of using URLSearchParams.
Suggested Fix
Create a pathSegment() helper and apply uniformly across all ~400 URL constructions.
function pathSegment(value: string): string {
  return encodeURIComponent(value);
}
// Usage: `/zones/${pathSegment(zoneId)}/dns_records/${pathSegment(id)}`
Flagged by: Claude, Gemini
5. Config temp file race condition
Critical 2-Agent Consensus
config.ts • writeConfig() / ensureConfigDir()
The temp file .config.json.tmp persists on crash. Also, ensureConfigDir() has a TOCTOU race between existsSync() and mkdirSync(). Temp filename is predictable (symlink attack vector).
Suggested Fix
Use PID-based temp names, add cleanup on failure, use idempotent mkdirSync({recursive: true}).
const tmpFile = join(CONFIG_DIR, `.config.json.tmp.${process.pid}`);
try {
  writeFileSync(tmpFile, content, { mode: 0o600 });
  renameSync(tmpFile, CONFIG_FILE);
} catch (err) {
  try { unlinkSync(tmpFile); } catch { }
  throw err;
}
Flagged by: Gemini, Ocasia
6. readConfig silently swallows ALL errors
Critical 2-Agent Consensus
config.ts • readConfig()
The catch block catches all errors and returns defaultConfig(). A permission-denied error on ~/.cf/config.json silently falls back to default, potentially using wrong credentials without warning.
Suggested Fix
Only swallow ENOENT, re-throw permission errors and other unexpected failures.
} catch (err: unknown) {
  if ((err as NodeJS.ErrnoException).code === "ENOENT") return defaultConfig();
  if (err instanceof SyntaxError) return defaultConfig();
  throw err; // Surface permission errors
}
Flagged by: Gemini, Claude
7. Missing input validation on API paths
Critical
client.ts • request()
The request() method constructs URLs via string interpolation (${BASE_URL}${path}) without validating the path parameter. An attacker controlling path input could inject query parameters or traverse paths.
Suggested Fix
if (!path.match(/^\/[a-z0-9\/_-]+$/i)) {
  throw new UsageError("Invalid API path");
}
Flagged by: Ocasia
8. IPv6 validation dangerously permissive
Critical
utils/validators.ts • validateIP()
The IPv6 check /^[a-f0-9:]+$/i accepts nonsense like :::::::: or a:b:c. Any string with colons and hex chars passes validation.
Suggested Fix
import { isIPv4, isIPv6 } from "net";
export function validateIP(value: string): string {
  const trimmed = value.trim();
  if (isIPv4(trimmed) || isIPv6(trimmed)) return trimmed;
  throw new UsageError(`Invalid IP address: "${trimmed}".`);
}
Flagged by: Gemini

Recommendations (Should Fix)

9. No User-Agent header sent
Should Fix 2-Agent Consensus
client.ts • getAuthHeaders()
No User-Agent means harder debugging with Cloudflare support, no per-client rate-limit tuning.
Fix
"User-Agent": `cf-cli/${VERSION} (Bun/${Bun.version})`
Flagged by: Claude, Ocasia
10. YAML serializer incomplete string escaping
Should Fix 2-Agent Consensus
output.ts • toYaml()
Only quotes strings containing \n, :, or #. Misses { [ > | & * ! % @ and YAML scalars like true, false, null. Use a YAML library or expand the quoting logic.
Flagged by: Gemini, Claude
11. AbortController timeout not cleaned up in all paths
Should Fix 2-Agent Consensus
client.ts • request() fetch loop
On retry paths, old AbortController objects are abandoned. Use a try/finally pattern to always clear the timeout.
Flagged by: Gemini, Ocasia
12. Flag naming inconsistency (kebab-case CLI vs camelCase internal)
Should Fix
args.ts • normalizeKey()
Invisible --account-idaccountId conversion creates a dual-track mental model with no compile-time safety. A typo silently falls through. Pick one canonical form or create typed flag definitions.
Flagged by: Claude
13. Negative numbers rejected as flag values
Should Fix
utils/args.ts • parseArgs()
--ttl -1 is treated as boolean flag ttl=true plus positional -1 because -1 starts with -. Check for /^-\d/ pattern to allow negative numbers.
Flagged by: Gemini
14. Add 503 to retryable status codes
Should Fix
client.ts • retry logic
HTTP 500/502/503 responses hit !response.ok and throw immediately without retrying. Transient 503s from Cloudflare during maintenance are common.
Fix
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
if (RETRYABLE.has(response.status) && attempt < MAX_RETRIES) continue;
Flagged by: Gemini
15. resolveZoneId doesn't handle multi-zone ambiguity
Should Fix
utils/zone-resolver.ts
When resolving domain → zone ID, takes the first result without confirming uniqueness. Multiple zones sharing the same name (multi-account) could silently select the wrong one.
Flagged by: Gemini
16. Bash completion covers only 7 of 80+ resources
Should Fix
completion/bash.ts
Hardcoded list: "zones dns accounts user cache config completion". Generate programmatically from the router or keep in sync with HELP_TEXT.
Flagged by: Gemini
17. parseInt silently drops floats and accepts malformed input
Should Fix
args.ts • getNumberFlag()
parseInt("3600.5", 10) returns 3600; parseInt("3600abc") returns 3600. Use Number(val) with Number.isInteger() check, or throw UsageError for non-integer values.
Flagged by: Claude
18. BillingProfile includes card_number as plain string
Should Fix 2-Agent Consensus
types/index.ts • BillingProfile
Type includes card_number?: string. If a billing endpoint response is passed to the detail renderer, the raw PAN could be emitted. Remove the field or annotate: /** Always masked by API; never log or display. */
Flagged by: Gemini, Claude
19. Destructive operations need explicit confirmation
Should Fix
dns/zones delete, purge commands
Commands like zones delete, dns delete, cache purge only have a --yes bypass. Consider requiring --confirm <resource-name> matching the target for destructive operations.
Flagged by: Ocasia
20. Pagination mixes page-based and cursor-based in same loop
Should Fix
client.ts • fetchAll()
page++ still increments after switching to cursor mode. If an API switches modes, this causes off-by-one requests. Separate the two modes clearly.
Flagged by: Gemini, Ocasia
21. config/set.ts reads config twice (TOCTOU)
Should Fix
config/set.ts
readConfig() called twice — once to check existence, once to merge. Race condition on concurrent writes. Call once at the top and reuse.
Flagged by: Gemini
22. workers/deploy.ts uses synchronous readFileSync
Should Fix
workers/deploy.ts
Blocks event loop for large worker scripts. Use Bun.file(file).text() (already used elsewhere in codebase).
Flagged by: Gemini
23. Confirm prompt fragile — use readline.createInterface
Should Fix 2-Agent Consensus
output.ts / prompts.ts • confirm()
Custom stdin handling has edge cases: race with raw mode, double setRawMode(false), stdin left resumed on Ctrl+C. Use readline.createInterface which handles all TTY edge cases.
Flagged by: Claude, Gemini
24. noColor flag logic has a subtle bug
Should Fix
index.ts
--no-color is parsed as color: false. The code also checks for noColor key which is never set from --no-color. Creates confusing asymmetry.
Flagged by: Gemini
25. resolveAccountId makes API call on every command
Should Fix
utils/account-resolver.ts
Every command without configured account_id calls GET /accounts, adding ~200-400ms latency. Add a TTL cache or prompt the user to configure it.
Flagged by: Gemini, Claude
26. Unhandled promise rejection in main()
Should Fix
index.ts • main()
Top-level .catch() writes to stderr but doesn't guarantee process.exit(1) in all code paths.
Flagged by: Ocasia

Observations (Nice to Have)

27. VERSION hardcoded to "0.1.0"
Observation
Not read from package.json. --version will lie after a version bump. Inject via build step or read at startup.
Flagged by: Claude
28. CSV output uses LF instead of CRLF
Observation
RFC 4180 requires CRLF. Excel on Windows may show all data on one row.
Flagged by: Claude
29. D1 query passes raw SQL without documentation note
Observation
Not a bug (SQL goes to Cloudflare's API). But add a comment so future maintainers don't add client-side SQL execution that would require parameterization.
Flagged by: Claude, Gemini
30. Error class hierarchy missing cause chaining
Observation
Custom error types don't use ES2022 cause option. Original stack traces lost when wrapping network errors. Consider surfacing in --verbose mode.
Flagged by: Gemini
31. getNestedValue handles arrays but table() does not
Observation
A path resolving to an array renders [object Array] in tables. The detail() renderer handles this correctly with .join().
Flagged by: Gemini
32. --version flag handling inconsistency
Observation
Both cf --version and cf version work, but cf dns --version routes to the dns command instead of printing the version.
Flagged by: Gemini
33. Test suite auto-confirms with yes: true globally
Observation
createTestContext defaults yes: true, silently bypassing confirmation prompts. Tests for abort paths need explicit { yes: undefined } override.
Flagged by: Gemini
34. Redundant prototype-pollution check in config/set.ts
Observation
Regex [a-zA-Z0-9][a-zA-Z0-9-]* already excludes __proto__, constructor, prototype (underscores). The subsequent RESERVED_KEYS check is dead code.
Flagged by: Claude
35. Bash completion requires bash-completion package
Observation
_init_completion may not be available on minimal systems (Alpine, CI). Provide a fallback.
Flagged by: Gemini
36. Config command auth bypass is fragile
Observation
The config resource is special-cased by catching AuthError. A cleaner approach: declare which commands require auth in the routing table upfront.
Flagged by: Claude
37. No declarative command spec / schema layer
Observation
400+ commands repeat the same parseArgs → getStringFlag → validate → resolve → call → output pipeline manually. A CommandSpec type would give compile-time flag safety, auto-generated help, and a single location for cross-cutting concerns like --dry-run.
Flagged by: Claude
38. Zero runtime dependencies — excellent
Observation
Bun-only with no external npm packages. Eliminates supply chain risk entirely. All 5 reviewers highlighted this as a strong architectural choice.
Flagged by: All 5 agents

Security Checklist

Architectural Notes

What Works Well

The layered architecture (index.ts → router → command module → utils) is clean and testable. The Context object threading pattern is idiomatic and avoids globals. The test infrastructure (createTestContext, mockClient) produces readable tests. Atomic config writes with renameSync show real production awareness. Credential resolution order is clearly documented. Zero runtime dependencies eliminate supply chain risk.

Design Concern — No Command Spec Layer

All 400+ commands repeat the same pipeline manually. No declarative command spec means: adding a global flag requires touching every command, flag documentation is only in help strings (not type-checked), and validation rules can silently diverge. A CommandSpec type would give compile-time safety with minimal refactoring.

Design Concern — kebab-to-camelCase Silent Normalization

The invisible --account-idaccountId conversion in normalizeKey creates a permanent dual-track mental model. A typo in the internal key name silently falls through to defaults. Prefer explicit typed flag definitions.

Prioritized Action Plan

P1 — Security (Fix First)

  1. Accept secrets via stdin/file/env, never CLI args (#1)
  2. Fix IPv6 validation with net.isIPv6() (#8)
  3. Add API path validation in request() (#7)
  4. Uniform encodeURIComponent on all URL path segments (#4)

P2 — Correctness (Fix Next)

  1. Respect Retry-After header on 429s (#2)
  2. Add MAX_PAGES guard to fetchAll (#3)
  3. Surface permission errors in readConfig (#6)
  4. Fix config temp file race condition (#5)
  5. Add 503 to retryable status codes (#14)

P3 — Robustness (Should Fix)

  1. Add User-Agent header (#9)
  2. Fix YAML string escaping (#10)
  3. Clean up AbortController timeouts (#11)
  4. Handle negative numbers in flag parsing (#13)
  5. Expand bash completion to all resources (#16)
  6. Fix parseInt to reject malformed input (#17)

P4 — Polish (When Time Permits)

  1. Read VERSION from package.json (#27)
  2. CSV CRLF line endings (#28)
  3. Consider CommandSpec layer for 400+ commands (#37)
  4. Switch confirm() to readline.createInterface (#23)

Review Metrics

Reviewers
5/5
Consensus Issues
12
Total Findings
38
Risk Level
MEDIUM
Agent Model Critical Recommendations Observations
Geminigemini-2.5-pro 7127
Claudeclaude-opus-4-6 486
Ocasiaqwen3.5:397b 680
Rexdevstral-2:123b 050
Philqwen3-coder:480b 050