Skip to content

Instantly share code, notes, and snippets.

@rickychilcott
Created April 28, 2026 10:17
Show Gist options
  • Select an option

  • Save rickychilcott/7ad7a8aef890321b71d336279af55838 to your computer and use it in GitHub Desktop.

Select an option

Save rickychilcott/7ad7a8aef890321b71d336279af55838 to your computer and use it in GitHub Desktop.
Cloudflare Transform Rules for agent-ready static sites (markdown negotiation + Link header)
#!/usr/bin/env bash
#
# Apply the 4 Cloudflare Transform Rules that complete the agent-readiness setup
# (Options D and E from the score-maximizer bundle):
#
# 1. URL rewrite — Accept: text/markdown → /<path>/index.md
# 2. Response header — *.md responses → Content-Type: text/markdown
# 3. Response header — homepage / → Link: <api-catalog>, <agent-skills>
# 4. Response header — /.well-known/api-catalog → Content-Type: application/linkset+json
#
# Idempotent: on each phase, GETs the existing entrypoint ruleset, removes any
# rules whose descriptions match ours, then PUTs the full set. Preserves any
# unrelated rules the user already had in those phases.
#
# Usage:
# export CLOUDFLARE_API_TOKEN=... # token with Zone:Rulesets:Edit on the zone
# export CLOUDFLARE_ZONE_ID=... # zone overview page in CF dashboard
#
# ./scripts/setup-cf-transform-rules.sh <host> [--dry-run]
# HOST=<host> ./scripts/setup-cf-transform-rules.sh [--dry-run]
#
# Examples:
# ./scripts/setup-cf-transform-rules.sh get.stokedhq.com
# ./scripts/setup-cf-transform-rules.sh www.causey.app --dry-run
# HOST=www.rickychilcott.com ./scripts/setup-cf-transform-rules.sh
#
# After applying, verify with:
# curl -sI -H 'Accept: text/markdown' https://<host>/ | grep -i content-type
# curl -sI https://<host>/ | grep -i ^link
# curl -sI https://<host>/.well-known/api-catalog | grep -i content-type
#
set -euo pipefail
# ---- arg parsing ----
DRY_RUN=0
HOST="${HOST:-}"
usage() {
echo "usage: $(basename "$0") <host> [--dry-run]" >&2
echo " HOST=<host> $(basename "$0") [--dry-run]" >&2
exit 2
}
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
-h|--help) usage ;;
--*) echo "unknown flag: $arg" >&2; usage ;;
*) HOST="$arg" ;;
esac
done
if [[ -z "$HOST" ]]; then
echo "✗ host is required (e.g. get.stokedhq.com)" >&2
usage
fi
: "${CLOUDFLARE_API_TOKEN:?Need CLOUDFLARE_API_TOKEN in env}"
: "${CLOUDFLARE_ZONE_ID:?Need CLOUDFLARE_ZONE_ID in env}"
API="https://api.cloudflare.com/client/v4"
AUTH="Authorization: Bearer ${CLOUDFLARE_API_TOKEN}"
# ---- preflight ----
echo "→ verifying zone access for host: ${HOST}"
zone_resp=$(curl -sS -H "$AUTH" "${API}/zones/${CLOUDFLARE_ZONE_ID}")
if [[ "$(jq -r .success <<<"$zone_resp")" != "true" ]]; then
echo "✗ zone lookup failed:" >&2
jq . <<<"$zone_resp" >&2
exit 1
fi
zone_name=$(jq -r .result.name <<<"$zone_resp")
echo " zone: ${zone_name}"
if [[ "$HOST" != "$zone_name" && "$HOST" != *".$zone_name" ]]; then
echo " ⚠️ warning: host '${HOST}' is not under zone '${zone_name}'. continuing anyway."
fi
# ---- helpers ----
# fetch_existing_rules <phase> — prints a JSON array of existing rules (empty if none).
# Cloudflare returns 10003 when no entrypoint ruleset exists yet; treat that as empty.
fetch_existing_rules() {
local phase="$1"
local resp
resp=$(curl -sS -H "$AUTH" "${API}/zones/${CLOUDFLARE_ZONE_ID}/rulesets/phases/${phase}/entrypoint")
if [[ "$(jq -r .success <<<"$resp")" == "true" ]]; then
jq '.result.rules // []' <<<"$resp"
return 0
fi
if jq -e '.errors[]? | select(.code == 10003)' <<<"$resp" >/dev/null; then
echo '[]'
return 0
fi
echo "✗ unexpected response from GET entrypoint for phase ${phase}:" >&2
jq . <<<"$resp" >&2
exit 1
}
# put_phase_rules <phase> <rules_json_array> — replaces the entire entrypoint.
put_phase_rules() {
local phase="$1"
local rules_json="$2"
local body
body=$(jq -n --argjson rules "$rules_json" '{rules: $rules}')
if (( DRY_RUN )); then
echo " (dry-run) PUT ${API}/zones/${CLOUDFLARE_ZONE_ID}/rulesets/phases/${phase}/entrypoint"
echo " body:"
jq . <<<"$body" | sed 's/^/ /'
return 0
fi
local resp
resp=$(curl -sS -X PUT \
-H "$AUTH" \
-H "Content-Type: application/json" \
--data "$body" \
"${API}/zones/${CLOUDFLARE_ZONE_ID}/rulesets/phases/${phase}/entrypoint")
if [[ "$(jq -r .success <<<"$resp")" != "true" ]]; then
echo "✗ PUT failed for phase ${phase}:" >&2
jq .errors <<<"$resp" >&2
exit 1
fi
echo " ✓ phase ${phase} updated, $(jq '.result.rules | length' <<<"$resp") rule(s) total"
}
# apply_phase <phase> <our_rules_json> — merge our rules with any existing
# (replacing by matching description), then PUT.
apply_phase() {
local phase="$1"
local our_rules="$2"
echo ""
echo "── phase: ${phase}"
local our_descriptions
our_descriptions=$(jq '[.[].description]' <<<"$our_rules")
local existing
existing=$(fetch_existing_rules "$phase")
local kept
kept=$(jq --argjson ours "$our_descriptions" '[.[] | select(.description as $d | ($ours | index($d)) == null)]' <<<"$existing")
local combined
combined=$(jq -n --argjson kept "$kept" --argjson ours "$our_rules" '$kept + $ours')
local existing_count kept_count ours_count
existing_count=$(jq 'length' <<<"$existing")
kept_count=$(jq 'length' <<<"$kept")
ours_count=$(jq 'length' <<<"$our_rules")
echo " existing rules: ${existing_count}, kept (unrelated): ${kept_count}, applying: ${ours_count}"
put_phase_rules "$phase" "$combined"
}
# ---- request-phase rules: the markdown URL rewrite ----
REQUEST_RULES=$(jq -n --arg host "$HOST" '[
{
description: "Markdown for Agents — URL rewrite",
expression: ("(http.host eq \"" + $host + "\" and any(http.request.headers[\"accept\"][*] contains \"text/markdown\") and not ends_with(http.request.uri.path, \".md\"))"),
action: "rewrite",
action_parameters: {
uri: {
path: {
expression: "concat(http.request.uri.path, \"index.md\")"
}
}
},
enabled: true
}
]')
# ---- response-headers-phase rules: 3 headers ----
LINK_VALUE='</.well-known/api-catalog>; rel="api-catalog", </.well-known/agent-skills/index.json>; rel="https://schemas.agentskills.io/discovery/0.2.0/"'
RESPONSE_RULES=$(jq -n --arg host "$HOST" --arg link "$LINK_VALUE" '[
{
description: "Markdown Content-Type",
expression: ("(http.host eq \"" + $host + "\" and ends_with(http.request.uri.path, \".md\"))"),
action: "rewrite",
action_parameters: {
headers: {
"content-type": {
operation: "set",
value: "text/markdown; charset=utf-8"
}
}
},
enabled: true
},
{
description: "Agent discovery Link header",
expression: ("(http.host eq \"" + $host + "\" and http.request.uri.path eq \"/\")"),
action: "rewrite",
action_parameters: {
headers: {
"link": {
operation: "set",
value: $link
}
}
},
enabled: true
},
{
description: "API catalog Content-Type",
expression: ("(http.host eq \"" + $host + "\" and http.request.uri.path eq \"/.well-known/api-catalog\")"),
action: "rewrite",
action_parameters: {
headers: {
"content-type": {
operation: "set",
value: "application/linkset+json"
}
}
},
enabled: true
}
]')
apply_phase "http_request_transform" "$REQUEST_RULES"
apply_phase "http_response_headers_transform" "$RESPONSE_RULES"
echo ""
echo "✅ done."
echo ""
echo "verify with:"
echo " curl -sI -H 'Accept: text/markdown' https://${HOST}/ | grep -i content-type"
echo " curl -sI https://${HOST}/ | grep -i ^link"
echo " curl -sI https://${HOST}/.well-known/api-catalog | grep -i content-type"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment