This guide will walk you through every step, from start to finish, to create your own GitHub repository with a workflow that periodically fetches the Hagezi Multi Pro++ blocklist, processes it, and updates your Cloudflare Gateway lists and policy for ad blocking. We'll use Python for the script because it's simple, requires minimal dependencies, and works well in GitHub Actions. The workflow will run daily (or manually) to keep your blocklists up to date.
I'll assume you're starting from scratch, but you've already created your Cloudflare API token for Gateway. If anything goes wrong, double-check the steps exactly as written—I've researched the APIs, blocklist format, limits, and similar setups to ensure accuracy. This is based on Cloudflare's API documentation, the Hagezi blocklist structure (plain domains, one per line, with # comments skipped), and best practices from similar repositories.
Important Notes Before Starting:
This setup uses Cloudflare's free Zero Trust plan. Limits: 1,000 domains per list, up to 300 lists per account (allowing up to 300,000 blocked domains total). The Hagezi Pro++ list has 293,000 entries, so it fits (split into 294 lists).
Cloudflare Gateway blocks exact hostnames (no wildcards supported in lists). The Hagezi list is compatible as it's a plain list of domains/hostnames.
No whitelists or additional blocklists—only the one you specified.
The workflow deletes old lists (named "Adblock_List_*") before creating new ones to avoid clutter and stay within limits.
After setup, you'll need to configure your devices to use Cloudflare Gateway DNS (e.g., via WARP client or custom DNS settings) for ad blocking to take effect. This guide focuses on the workflow and API updates.
If you encounter API errors, check your token permissions (must include "Account: Zero Trust: Edit") and account ID.
GitHub Actions is free for public repos; private is fine too.
The script processes the list: skips comments (# lines), trims whitespace, removes duplicates, and splits into chunks of 1,000 domains each.
Cloudflare Account ID:
Log in to the Cloudflare Dashboard: https://dash.cloudflare.com/
Go to any section (e.g., Zero Trust > Gateway).
On the right sidebar, under "Account," copy your Account ID (a 32-character string like 0da42c8d2132a435071ac0bfdaed6cbd).
Cloudflare API Token (you already have this, but verify):
If needed, create a new token:
Click "Create Token" > Use "Edit Zero Trust" template (or custom).
Permissions: Account > Zero Trust > Edit (include Read too).
Account: Select your account.
No zone or client IP restrictions needed.
Copy the token (starts with "Bearer" but you just need the long string).
Verify Gateway Setup:
In Cloudflare Dashboard, go to Zero Trust > Gateway > DNS.
Ensure you have a location set up (default is fine for global DNS).
No need to create lists or policies manually—we'll do it via API.
Log in to GitHub: https://github.com/
Click the "+" icon in the top right > "New repository".
Name it something like cloudflare-gateway-adblock-updater.
Description: "GitHub Actions workflow to update Cloudflare Gateway with Hagezi Pro++ adblock list".
Make it public or private (your choice—public is free for Actions).
Don't add a README or .gitignore yet.
Click "Create repository".
Secrets store sensitive info like your API token.
In your new repo, go to "Settings" (top tab).
On the left, expand "Secrets and variables" > Click "Actions".
Click "New repository secret".
Add these two secrets:
CLOUDFLARE_API_TOKENName:
Value: Paste your API token string.
CLOUDFLARE_ACCOUNT_IDName:
Value: Paste your Account ID.
Click "Add secret" for each.
This script does the heavy lifting: fetches the blocklist, processes it, deletes old lists, creates new chunked lists, and sets up/updates the blocking policy.
In your repo, click "Add file" > "Create new file".
Name it: update_adblock.py
Paste the following code exactly:
import requests
import json
import os
import sys
# Get env vars from GitHub secrets
api_token = os.environ.get('CLOUDFLARE_API_TOKEN')
account_id = os.environ.get('CLOUDFLARE_ACCOUNT_ID')
if not api_token or not account_id:
print("Error: Missing API token or account ID.")
sys.exit(1)
# API base URL
base_url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/gateway"
headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json"
}
# Step 1: Fetch the Hagezi Pro++ blocklist
blocklist_url = "https://gitlab.com/hagezi/mirror/-/raw/main/dns-blocklists/wildcard/pro.plus-onlydomains.txt"
response = requests.get(blocklist_url)
if response.status_code != 200:
print(f"Error fetching blocklist: {response.status_code}")
sys.exit(1)
# Process the list: skip comments, trim, unique
lines = response.text.splitlines()
domains = set() # Use set for unique
for line in lines:
line = line.strip()
if line and not line.startswith('#'):
domains.add(line)
domains = list(domains)
print(f"Fetched and processed {len(domains)} unique domains.")
# Step 2: Split into chunks of 1000 (free plan limit per list)
chunk_size = 1000
chunks = [domains[i:i + chunk_size] for i in range(0, len(domains), chunk_size)]
print(f"Split into {len(chunks)} chunks.")
# Step 3: Delete old lists (named Adblock_List_*)
response = requests.get(f"{base_url}/lists", headers=headers)
if response.status_code != 200:
print(f"Error getting lists: {response.text}")
sys.exit(1)
lists = response.json()['result']
for lst in lists:
if lst['name'].startswith('Adblock_List_'):
delete_response = requests.delete(f"{base_url}/lists/{lst['id']}", headers=headers)
if delete_response.status_code == 200:
print(f"Deleted old list: {lst['name']}")
else:
print(f"Error deleting list {lst['name']}: {delete_response.text}")
# Step 4: Create new lists
list_names = []
for i, chunk in enumerate(chunks, 1):
list_name = f"Adblock_List_{i}"
data = {
"name": list_name,
"type": "DOMAIN",
"description": "Hagezi Pro++ Adblock Chunk",
"items": [{"value": domain} for domain in chunk]
}
response = requests.post(f"{base_url}/lists", headers=headers, data=json.dumps(data))
if response.status_code == 200:
print(f"Created list: {list_name} with {len(chunk)} items.")
list_names.append(list_name)
else:
print(f"Error creating list {list_name}: {response.text}")
sys.exit(1)
# Step 5: Create or update the DNS blocking policy
# Build expression: hostname in $Adblock_List_1 or hostname in $Adblock_List_2 or ...
if list_names:
expression = " or ".join([f'hostname in ${name}' for name in list_names])
else:
print("No lists created. Skipping policy.")
sys.exit(0)
# Check if policy exists
response = requests.get(f"{base_url}/rules", headers=headers)
if response.status_code != 200:
print(f"Error getting rules: {response.text}")
sys.exit(1)
rules = response.json()['result']
adblock_rule = next((rule for rule in rules if rule['name'] == 'Block Ads'), None)
data = {
"action": "block",
"description": "Block ads using Hagezi Pro++ list",
"enabled": True,
"filters": ["dns"],
"name": "Block Ads",
"traffic": expression
}
if adblock_rule:
# Update existing rule
rule_id = adblock_rule['id']
response = requests.put(f"{base_url}/rules/{rule_id}", headers=headers, data=json.dumps(data))
if response.status_code == 200:
print("Updated existing Block Ads policy.")
else:
print(f"Error updating policy: {response.text}")
sys.exit(1)
else:
# Create new rule
response = requests.post(f"{base_url}/rules", headers=headers, data=json.dumps(data))
if response.status_code == 200:
print("Created new Block Ads policy.")
else:
print(f"Error creating policy: {response.text}")
sys.exit(1)
print("Update complete!")
Click "Commit new file" (use default message).
Explanation of the Script:
Fetches the exact URL you provided.
Processes: Removes duplicates, skips # comments and empty lines.
Splits into 1,000-domain chunks (free plan limit).
Deletes old matching lists to clean up.
Creates new lists (type: DOMAIN).
Builds a policy expression linking all lists.
Creates or updates a single DNS policy to block hostnames in those lists.
This YAML file defines the automation: runs daily, sets up Python, installs requests, and runs the script.
In your repo, click "Add file" > "Create new file".
Name it: .github/workflows/update-adblock.yml (note the folders—GitHub will create them).
Paste the following code exactly:
name: Update Cloudflare Gateway Adblock
on:
schedule:
- cron: '0 0 * * *' # Runs every day at midnight UTC
workflow_dispatch: # Allows manual runs
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Run update script
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: python update_adblock.py
Click "Commit new file".
Explanation of the Workflow:
Triggers: Daily at midnight UTC, or manually via GitHub.
Steps: Checks out code, sets up Python 3.12, installs requests library, runs your script with secrets as env vars.
In your repo, go to "Actions" tab.
If prompted, enable GitHub Actions (click "I understand..." and enable).
You should see "Update Cloudflare Gateway Adblock" listed.
To test manually: Click the workflow > "Run workflow" > "Run workflow" (green button).
Watch the run: Click the running job to see logs. It should fetch, process, create lists, and set the policy.
If successful, you'll see messages like "Created list: Adblock_List_1" and "Created new Block Ads policy."
If errors (e.g., auth), check secrets and token permissions.
Verify in Cloudflare:
Go to Zero Trust > Reusable components > Lists: See "Adblock_List_1" to "Adblock_List_294" (approx.).
Go to Zero Trust > Gateway > Policies > DNS: See "Block Ads" policy with the long expression.
The workflow runs automatically daily. If the blocklist hasn't changed much, it still updates (to catch any updates).
To actually block ads:
In Cloudflare Dashboard: Zero Trust > Gateway > DNS locations > Note your DNS over HTTPS endpoint (e.g., https://your-team-name.cloudflare-gateway.com/dns-query).
On devices:
Desktop/Mobile: Install Cloudflare WARP client (free), enroll in your Zero Trust team.
Manual DNS: Set DNS to your Gateway's DoH/DoT endpoint (use apps like DNSCloak on iOS or system settings).
For routers: Configure DNS to point to Gateway IPs (find via dig your-gateway.cloudflare-gateway.com).
Test: Visit a site with ads (e.g., speedtest.net)—ads should be blocked via DNS failure.
API Errors: 401 = bad token; 403 = wrong permissions; 400 = bad data (check expression length if >300 lists, but unlikely).
Too Many Lists: If over 300, reduce chunk size? But Hagezi is under.
Script Fails on Fetch: Check the URL is accessible (it is public).
No Changes Detected: The script always runs fully.
Actions Not Running: Ensure repo settings allow Actions (Settings > Actions > General > Allow all actions).
Logs: Always check Actions logs for details.
If stuck, re-run manually or check Cloudflare API status.
This should get everything set up without further help. Your ad blocking is now automated! If the Hagezi list updates, your Gateway will reflect it daily.