Skip to content
Back to skills

Cf Token Scope

BSecurity

This skill should be used to widen an existing Cloudflare API token's scope via Playwright dashboard automation, then run the ADR-130 retained-scope probe set verifying the target scope was added and none was dropped.

  • 16 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 3, 2026
ai-agentsrustshellbashterraformgitapi

Works with

  • claude code
  • cli
  • api
  • mcp

Security analysis

B75/100
  • criticalSends environment variables or credentials to an external URL

Pro scans all 3 files and shows the line behind each finding

Scanned September 24, 2026

npx -y skills add jikig-ai/soleur --skill cf-token-scope --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Cf Token Scope?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Cf Token Scope
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/jikig-ai-cf-token-scope/badge)](https://www.skillsdirectory.com/skills/jikig-ai-cf-token-scope)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: cf-token-scope
description: This skill should be used to widen an existing Cloudflare API token's scope via Playwright dashboard automation, then run the ADR-130 retained-scope probe set verifying the target scope was added and none was dropped.
disable-model-invocation: true
---

<!-- soleur-cloud-mode:start -->
**Cloud Mode (Devin):** before pipeline work run `bash "${CLAUDE_PLUGIN_ROOT}/scripts/cloud-detect.sh"`. If `CLAUDE_PLUGIN_ROOT` is unset (cloud exec shells do not export it), resolve the root by IDENTITY, never by script basename: for `d` in `"$HOME/.local/share/devin/cli/plugins/cache"` and `/opt/.devin/plugins`, skip unless `[ -d "$d" ]`, then `MANIFEST="$(find "$d" -path '*/.claude-plugin/plugin.json' -exec grep -l '"name"[[:space:]]*:[[:space:]]*"soleur"' {} + 2>/dev/null | head -1)"`, `ROOT="${MANIFEST%/.claude-plugin/plugin.json}"` — one resolution, two consumers: `$ROOT/scripts/cloud-detect.sh` and `$ROOT/scripts/precommit-guard.sh`. (Shape check, not authentication: a planted `{"name":"soleur"}` dir passes — ADR-179 A11.) `local` or `not-local:no-devin-env` proceeds normally; any other `not-local:<reason>` applies `<plugin-root>/devin/INSTRUCTIONS.md` §Cloud Mode: emit the `--banner`, fan out sequentially with `Reviewed-Coverage: sequential-fallback` disclosure (never claim an independent review ran), `message_user` ack before any secrets read or production mutation, and run `precommit-guard.sh` before any `git commit` — hooks do not fire in cloud.
<!-- soleur-cloud-mode:end -->

<!-- grok-harness-invoke:start -->
**Grok Build (`plugins/soleur/lib/harness.ts` `invokeSkill()`):** Read this SKILL.md in this process and run it to completion. A one-segment `soleur:<name>` in this document names a SKILL — on Grok Build, Read `plugins/soleur/skills/<name>/SKILL.md` in this process; it is not a nested tool_use. A multi-segment id such as `soleur:<domain>:<name>` names an AGENT: spawn it, never Read it, and on Grok Build spawn_subagent takes the id with its colons replaced by hyphens (`agentIdToGrokSubagentType`). **Claude Code:** Skill tool for a skill (`soleur:<name>`), Task tool with `subagent_type` for an agent. Forbidden is executing a subset, not the Read.
<!-- grok-harness-invoke:end -->

# cf-token-scope

Close the capability gap ADR-130 leaves open: there is no first-party path for
Cloudflare API-token scope changes, so each one is an ad-hoc dashboard trip
(third on record — #6657 DNS, #6649 R2, #6755 Config Rules). This skill widens a
token via **Playwright MCP** dashboard automation and makes the ADR-130
retained-scope check a deterministic, fail-closed command.

The widen mutates a live production credential that four concerns depend on. A
dashboard "save" that **replaces** rather than **appends** scopes silently breaks
cache rules, WAF, single redirects, transform rules, and account bulk redirects
at once — so the retained-scope probe is the load-bearing half, not the widen.

## Usage

The deterministic core is a read-only probe script,
[cf-token-scope.sh](./scripts/cf-token-scope.sh). It only ever probes — run it as
often as needed; it mutates nothing.

### Preflight: verify the plugin install before any snapshot

The redactor is reached through `${CLAUDE_PLUGIN_ROOT}`. An ambient value pointing at a
directory that is not a Soleur install would resolve to a path that does not exist — or, worse,
to one an attacker chose. Verify plugin IDENTITY and halt if it does not hold (ADR-179 decision 2);
a `test -f` on the script alone is a shape check and was measured bypassable.

```bash
[ -f "${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json" ] \
  && grep -q '"name"[[:space:]]*:[[:space:]]*"soleur"' "${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json" \
  || { echo "SOLEUR_SNAPSHOT_HALT reason=plugin-root-unverified root=[${CLAUDE_PLUGIN_ROOT}]" >&2
       echo "  Cannot locate the snapshot redactor, so no accessibility snapshot may be taken here." >&2
       echo "  Root EMPTY: no Soleur plugin is loaded in this session. Install it and start a NEW session." >&2
       echo "  Root set but wrong: a repo checkout is not an install. Run 'claude plugin update soleur@soleur-marketplace' (or the id 'claude plugin list' prints, if you added the repository directly), then RESTART Claude Code." >&2
       echo "  Nothing has been captured yet, so nothing has leaked." >&2
       exit 2; }
```

```bash
# Baseline / re-check — probe the four ADR-130 retained scopes:
bash "${CLAUDE_PLUGIN_ROOT}/skills/cf-token-scope/scripts/cf-token-scope.sh"

# After a widen — also assert the newly-added scope is live:
bash "${CLAUDE_PLUGIN_ROOT}/skills/cf-token-scope/scripts/cf-token-scope.sh" \
  --target-entrypoint http_config_settings

# Print the probe commands without running them (token stays unexpanded):
bash "${CLAUDE_PLUGIN_ROOT}/skills/cf-token-scope/scripts/cf-token-scope.sh" --dry-run
```

The token / zone / account (`CF_API_TOKEN_RULESETS`, `CF_ZONE_ID`,
`CF_ACCOUNT_ID`) and Doppler config (`soleur` / `prd_terraform`) are hardcoded:
the ADR-130 retained-scope URL set is meaningful only for the rulesets token, so
a `--token-var` knob pointed elsewhere would run the wrong URLs against the wrong
token.

## Execution — the 3-step widen flow

1. **Pre-widen baseline.** Run the probe. Before the widen, the target
   entrypoint reads `403` (scope absent) while the known-granted controls read
   the authorized signal. Record this output — the added-scope check in step 3
   compares against it.

2. **Widen (Playwright MCP).** Follow
   [widen-playbook.md](./references/widen-playbook.md): decide widen-vs-mint per
   ADR-130, navigate to `https://dash.cloudflare.com/profile/api-tokens`, hand
   off only the login/MFA gate to the operator, then edit the token —
   three-dot menu → Edit → Add more → select the permission (same API family) →
   Continue to summary → Update token. Editing permissions does **not** rotate
   the token value, so no Doppler write follows.

   The widen transits a **full-power dashboard session** (the cookie is an
   account-wide bearer). Do **not** dump `browser_network_requests` /
   `browser_console_messages` to files, scope screenshots to the edit control,
   take any accessibility snapshot in the file form and filter the file through
   the redactor (`"${CLAUDE_PLUGIN_ROOT}/skills/agent-browser/scripts/redact-a11y-snapshot.py"`)
   — an MCP tool result cannot be piped through a script; the playbook states
   when a bare call is safe — and
   always call `browser_evaluate` **with** a `filename` — without one the value
   is returned into the transcript. This corrects an inverted instruction that
   stood here previously; see the playbook's leak constraints.

3. **Post-widen verification.** Re-run the probe with
   `--target-entrypoint <phase>`. Success = the target is currently authorized AND
   every retained control stayed authorized AND the account-scheme control is an
   authorized `200` — exit 0, `PASS: target scope present` + `PASS: no scope
   dropped`. A single run cannot observe the `403 → authorized` *transition*, so
   the tool prints "present", not "added" — confirm the transition against your
   step-1 baseline.

## Probe-set contract (three-layer fail-closed classifier)

The probe set is the ADR-130 four-probe retained set plus Zone WAF — four **zone**
phases (`http_config_settings`, `http_request_dynamic_redirect`,
`http_request_cache_settings`, `http_request_firewall_custom`) and one **account**
list (`accounts/<acct>/rulesets`). The classifier captures each response **body**
(not `-o /dev/null`) and decides in three layers:

1. **Status** — `403` / `000` / `5xx` / empty / no-newline / non-numeric = FAIL.
2. **Body-shape** — a `200` passes only when the body has `success == true` AND
   `.result` is an array. A degraded `200` (`{"success":false,...}` OR
   `{"success":true,"result":null}`) is a FAIL. Never key on `.result | length` —
   jq reads `null` as `0` and would pass.
3. **Per-scheme control** — the account list is the account-scheme control and
   must be an authorized `200` (an account `404` = FAIL). A zone `404` (ADR-130's
   "phase exists, empty") is trusted **only** for `http_config_settings` — the one
   phase whose 403-on-missing-scope semantics ADR-130 empirically verified — and
   only under an authorized zone control (`http_request_dynamic_redirect`). Every
   other phase's `404` fails closed: 403-on-missing is unverified there, so a
   dropped scope that returned `404` must not read as green.

## Exit codes

- `0` — every retained scope authorized (and the target, if given).
- `2` — usage error, missing prerequisite (`curl` / `doppler` / `jq`), or an absent Doppler secret.
- `3` — probe failed: a scope was dropped, degraded, or denied.

## Sharp Edges

- **The probe set is a CANARY for the whole-list REPLACE failure mode, not
  exhaustive per-permission coverage.** A dashboard save that replaces (not
  appends) drops *all* scopes at once → every probe catches it. Zone WAF
  (`http_request_firewall_custom`) is now probed, but Transform Rules
  (`http_response_headers_transform`) and account Filter Lists are not — a
  *surgical* single-permission drop of one of those can still pass. Extend the
  entrypoint set if a future threat model needs per-permission coverage.
- **The probe attests read reachability, not `:Edit` retention.** It issues `GET`,
  so a dashboard REPLACE that re-adds a phase as **Read-only** (a one-click
  operator error — the CF dropdown lists Read/Edit side-by-side) returns an
  authorized `200` and reads as green while the write/hijack capability was
  dropped. Visually confirm the widened permission is `Edit`, not `Read`.
- **Probe, never trust the Cloudflare UI permission label** — it is named
  inconsistently across surfaces. The target entrypoint returning non-403 is the
  ground truth (ADR-130).
- **Update the scope ledger.** After a widen, add the new permission to the
  `apps/web-platform/infra/variables.tf` description of the widened token (ADR-130's
  scope ledger), in the feature PR that consumes the widen.
- **Never print the Bearer token.** The script passes it from a private fd (not
  argv), `--dry-run` leaves it unexpanded, and it runs no `set -x`. When driving
  the widen, keep the full-power-session leak constraints above.
- **New phase → enumerate before apply.** If the widen enables a *new* ruleset
  phase, confirm its entrypoint is `404`/empty before any infra apply, or a
  whole-list ruleset create clobbers dashboard rules (ADR-130, ADR-136).

## Related

- [ADR-130](../../../../knowledge-base/engineering/architecture/decisions/ADR-130-cloudflare-token-widen-vs-narrow-alias.md) — widen-vs-narrow-alias rule, the four-probe retained-scope set, and the capability gap this skill closes.
- [ADR-136](../../../../knowledge-base/engineering/architecture/decisions/ADR-136-preapply-entrypoint-enumeration-gate.md) — the standing pre-apply entrypoint-enumeration gate.
- [provision-cloudflare](../provision-cloudflare/SKILL.md) — mints *tenant* tokens (a distinct function; requires `User API Tokens:Edit`, which no Soleur token holds).
- [widen-playbook.md](./references/widen-playbook.md) — the Playwright MCP click-path and leak constraints.
- Learnings: `2026-03-21-cloudflare-api-token-permission-editing.md` (the Playwright widen path), `2026-07-23-live-api-fail-closed-guard-counts-degraded-200-as-empty-and-control-probe-must-cover-every-scheme.md` (the three-layer classifier).

Files in this skill

  • SKILL.md7.7 KB
  • references/widen-playbook.md3.9 KB
  • scripts/cf-token-scope.sh10.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.