Overview
If you run more than one Pi-hole on your network, you’ve probably faced the same frustration: keeping blocklists in sync across all of them is tedious, error-prone, and easy to forget. Each Pi-hole ends up with a slightly different set of lists, maintained separately, with no version history and no easy way to roll back changes.
SyncBlock solves this by treating your blocklist configuration the same way a developer treats code — with version control, a central repository, and automated deployment.
The idea is simple: one authoritative list of blocklist sources lives in a private Git repository on Codeberg. Each Pi-hole runs a script on a daily cron schedule that fetches that list and synchronises its own Pi-hole database accordingly. Change the list once, and all your Pi-holes pick it up automatically.
The Architecture
The setup consists of two private Codeberg repositories and a sync script deployed to each Pi-hole.
pihole-lists is the repository containing youShallNotPass.txt — a plain text file with one blocklist URL per line. This is the single source of truth for what gets blocked across your entire network. You manage it from your Windows PC using TortoiseGit, committing changes and pushing them to Codeberg.
SyncBlock is the repository containing the tooling — the sync script, installer, and configuration template. You clone this onto each Pi-hole and run the installer once.
The flow looks like this:
- You edit
youShallNotPass.txton your PC and push to Codeberg - Each Pi-hole’s cron job runs
sync-adlists.shat 04:17 AM - The script fetches
youShallNotPass.txtusing a Personal Access Token - It updates Pi-hole’s
gravity.dbdatabase with the new set of adlists - It runs
pihole -gto rebuild gravity - If anything goes wrong, it automatically restores from a backup
Key Design Decisions
Private repositories. Both repos are kept private. Knowing what a system blocks — and what it doesn’t — is useful information for an attacker. There’s no good reason to make that public.
Token-based authentication. The sync script authenticates to Codeberg using a Personal Access Token stored in /etc/syncblock/codeberg.token, readable only by root. The token is never stored in the repository itself — it’s created manually on each Pi-hole and installed securely by the installer.
Non-destructive database updates. Rather than wiping Pi-hole’s adlist table and starting fresh, SyncBlock only manages entries it created — identified by the comment tag managed by repo. Manually added adlists are left completely untouched.
Transactional SQL. All database changes happen inside a single transaction. If anything fails mid-way, the database is left in its original state. A full backup is also taken before every sync, with automatic restore on failure.
Atomic file installation. Every file the installer places on the system is written to a temporary file first and then atomically renamed into place. There is no window where a partial or corrupted file could exist at the target path.
The Sync Script
The heart of SyncBlock is sync-adlists.sh. Here’s what it does in order:
1. Security checks. Before doing anything, it verifies that the config file and token file are owned by root, not writable by others, and not symlinks. This prevents privilege escalation attacks.
2. Fetch the adlist file. It downloads youShallNotPass.txt from your private Codeberg repo using curl with your token:
bash
curl --fail --silent --show-error \
--proto '=https' \
--tlsv1.2 \
--header "Authorization: token ${CODEBERG_TOKEN}" \
--output "$tmp_raw" \
"$ADLISTS_URL"
Note the --proto '=https' --tlsv1.2 flags — this enforces HTTPS and a minimum TLS version, preventing the script from being tricked into fetching over an insecure connection.
3. Validate every URL. Each line is checked to be a valid http:// or https:// URL before it gets anywhere near the database. Blank lines and comments are ignored.
4. Build and execute a SQL plan. Rather than inserting URLs one by one, SyncBlock builds a complete SQL transaction using a temporary table:
sql
BEGIN IMMEDIATE TRANSACTION;
CREATE TEMP TABLE desired_adlists(address TEXT PRIMARY KEY);
-- insert desired URLs into temp table
DELETE FROM adlist WHERE comment = 'managed by repo'
AND address NOT IN (SELECT address FROM desired_adlists);
INSERT INTO adlist(address, enabled, comment)
SELECT address, 1, 'managed by repo' FROM desired_adlists
WHERE NOT EXISTS (SELECT 1 FROM adlist WHERE address = desired_adlists.address);
COMMIT;
```
This means only genuinely new URLs are inserted, only removed URLs are deleted, and everything else is left alone — including any adlists you've added manually outside of SyncBlock.
**5. Run `pihole -g`.** After the database is updated, Pi-hole's gravity rebuild is triggered to fetch the actual blocklists and rebuild the DNS blocking database.
**6. Log a summary.** Every run logs a line like:
```
Summary: before=11 after=12 added=1 removed=0 unchanged=11
This makes it easy to see at a glance what changed and when.
The Installer
install.sh is run once per Pi-hole. It:
- Resolves all binary paths dynamically using
command -v— avoiding hardcoded paths that differ between Raspberry Pi OS versions - Fixes source file permissions before copying — files created on Windows often arrive with group-write bits set, which the security checks would correctly reject
- Installs the sync script to
/usr/local/bin/with mode700 - Creates
/etc/syncblock/and installs config and token with appropriate permissions - Runs a dry-run before installing the cron job — if the dry-run fails, the cron job is not installed and the reason is clearly reported
- Installs the cron job to
/etc/cron.d/pihole-adlist-syncat 04:17 AM daily
A --force-config flag allows replacing an existing config and token on reinstall, while the default behaviour preserves them.
Installation Walkthrough
Prerequisites
On each Pi-hole, ensure the following are available:
bash
sudo apt install curl sqlite3 git
Set up an SSH key
Each Pi-hole needs its own SSH key to clone from Codeberg:
bash
ssh-keygen -t ed25519 -C "pihole-hostname" -f ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub
Add the public key to Codeberg → Settings → SSH / GPG Keys.
Then configure SSH to always use it:
bash
vi ~/.ssh/config
```
```
Host codeberg.org
IdentityFile ~/.ssh/id_ed25519
User git
Test:
bash
ssh -T git@codeberg.org
Create a Personal Access Token
In Codeberg → Settings → Applications → Access Tokens, create a token with repository read permission only. Copy it — you’ll need it in a moment.
Clone and install
bash
cd /tmp
git clone ssh://git@codeberg.org/<yourname>/SyncBlock.git
cd SyncBlock
echo 'your-token-here' > codeberg.token
chmod 400 codeberg.token
vi sync-adlists.conf # replace <yourname> with your Codeberg username
sudo ./install.sh
Run a live sync
bash
sudo /usr/local/bin/sync-adlists.sh
Clean up
bash
cd /tmp && rm -rf SyncBlock
Repeat on each Pi-hole. The token is the same for all of them.
Managing Your Blocklists
All blocklist management happens in youShallNotPass.txt in your pihole-lists repo. Edit it on your Windows PC using TortoiseGit, commit, and push. Your Pi-holes will pick up the changes at their next scheduled sync, or immediately if you trigger a manual run.
A few things to keep in mind:
- All URLs must point to raw file content, not browser view pages. On Codeberg the raw URL format is
https://codeberg.org/<user>/<repo>/raw/branch/main/<filepath>— noterawnotsrc. - Pi-hole fetches each blocklist URL directly during
pihole -gand has no way to authenticate to private repositories. All blocklist sources inyouShallNotPass.txtmust therefore be publicly accessible. - You can add comments to
youShallNotPass.txtby starting a line with#. These are ignored by the sync script.
Logs and Troubleshooting
All sync activity is logged to /var/log/sync-adlists.log:
bash
tail -f /var/log/sync-adlists.log
Logs are rotated weekly, keeping 4 weeks of history — managed automatically by a logrotate config that the sync script installs and maintains itself.
If a sync fails, Pi-hole’s gravity.db is automatically restored from the most recent backup in /var/backups/pihole-adlist-sync/. Backups are pruned automatically, keeping the 10 most recent by default.
For a non-destructive preview of what a sync would change:
bash
sudo /usr/local/bin/sync-adlists.sh --dry-run
The Repository
The full source code for SyncBlock — including sync-adlists.sh, install.sh, and sync-adlists.conf — is available on Codeberg. The repository is currently private, but feel free to reach out if you have questions or want to discuss the implementation.
This article is part of my homelab documentation series.
