EN
  • English
  • Deutsch
  • Polski
  • Italiano
  • Español

Importing configuration files

When you set up purge-warden for the first time — or migrate from Pi-hole or AdGuard Home — you usually have several sources of truth to bring together: a list of devices on your LAN, the curated blocklists you want to subscribe to, the filtering profiles you want to apply, and the groups or schedules that tie them together. Rather than pasting everything into one file, purge-warden supports a modular include layout that lets each kind of input live in its own file (or per-cohort group of files). This page walks through how to populate each one.

The conceptual model and field reference for every entity below live in ./CONFIG_GUIDE.public.md. This page is operational: where do files go, what do they look like, which command brings them in.

Overview

A purge-warden install is driven by a single master file (/etc/purge-warden/config.toml) plus optional includes — sub-directories of small TOML files that the master pulls in by glob:

/etc/purge-warden/config.toml
toml
schema_version = 1

[server]
listen = "0.0.0.0:53"
default_profile = "default"
allow_from = ["192.168.1.0/24", "127.0.0.0/8"]

includes = [
  "devices.d/*.toml",
  "subnets.d/*.toml",
  "profiles.d/*.toml",
  "groups.d/*.toml",
  "schedules.d/*.toml",
  "blocklists.d/*.toml",
]
Each subdirectory holds one or more TOML fragments that contribute to the same logical section. The daemon merges them at load time. You may freely mix the layouts:

  • Monolithic — everything in config.toml. Fine for very small setups (under ~20 devices).
  • Split — master + *.d/ subdirectories. Recommended once you have multiple cohorts, multiple operators, or want clean git diffs.
Tip
every CLI write is also valid input for the include layout. You can hand-edit a fragment, run warden config lint, and reload — or you can let warden <verb> ... --into <file> write the fragment for you. Both paths produce the same TOML.

Importing a clients list

A “client” in purge-warden is a [[devices]] entry: a host on your LAN identified by IP and/or MAC, optionally bundled into one or more groups. To import many of them, drop one file per cohort under devices.d/.

TOML — devices.d/family.toml:

/etc/purge-warden/devices.d/family.toml
toml
[[devices]]
id = "edo-laptop"
display_name = "Edoardo's laptop"
ip = "192.168.1.10"
mac = "02:00:00:11:22:33"
profile = "default"
owner = "Edoardo"

[[devices]]
id = "daughter-iphone"
display_name = "Daughter's iPhone"
mac = "02:00:00:44:55:66"
mac_aliases = ["02:00:00:77:88:99"]
groups = ["kids"]
CLI equivalent (writes the same shape, choosing the target file with --into):

warden device add edo-laptop \
  --ip 192.168.1.10 --mac "02:00:00:11:22:33" \
  --profile default --display-name "Edoardo's laptop" \
  --owner Edoardo \
  --into devices.d/family.toml

warden device add daughter-iphone \
  --mac "02:00:00:44:55:66" \
  --mac-aliases "02:00:00:77:88:99" \
  --groups kids --display-name "Daughter's iPhone" \
  --into devices.d/family.toml

After hand-editing the file, validate and apply:

warden config lint                       # exit 0 if valid
sudo systemctl reload purge-warden       # SIGHUP — no downtime
Info
modern phones (iOS, Android) randomise their MAC per network. Register every MAC you’ve observed in mac_aliases so the device stays recognised.

Field reference

Most setups only need a handful of fields per device — typically id, display_name, mac (or ip), and either profile or groups. The table below covers every field the daemon recognises, grouped by purpose so you know where to reach when a more specific need comes up.

Identification

FieldTypeRequiredDefaultDescription
idstringyesUnique identifier referenced by groups, subnets, schedules, and CLI commands. Stable — don’t rename once it’s in use.
display_namestringyesHuman-readable label shown in the TUI, CLI output, and audit log.
ipIPv4 / IPv6noStatic IP pin. Skip if you only identify the device by MAC.
macstring XX:XX:XX:XX:XX:XXnoPrimary MAC address; format validated at load.
mac_aliaseslist of MAC stringsno[]Additional MACs for devices that rotate (iOS / Android private wifi). Any match identifies the device.

Profile assignment

FieldTypeRequiredDefaultDescription
profileprofile idnoDirect profile assignment. Wins over groups and subnets in the resolver chain.
groupslist of group idsno[]Group memberships. Used only when no direct profile is set; the highest-priority group wins.
Tip
start with one of profile or groups, not both. The resolver always prefers profile if it’s set, so a groups entry alongside it is dead weight.

Per-device rule overlays

FieldTypeRequiredDefaultDescription
allow_ruleslist of admin-rule idsno[]Per-device allow overlay. Checked before profile rules. Combined cap with deny_rules: 64 soft / 128 hard.
deny_ruleslist of admin-rule idsno[]Per-device deny overlay. Additive over profile-level allows. Same cap as allow_rules.
override_profile_denyboolnofalseWhen true, a per-device allow_rules entry can override a profile-level deny on the same domain. Use sparingly — every override surfaces in the audit log.

Metadata (informational only, no effect on filtering)

FieldTypeRequiredDefaultDescription
tagslist of stringsno[]Free-form labels for filtering in the TUI/CLI (e.g. ["wifi", "guest"]).
ownerstringnoWho’s responsible for the device.
device_typestringnoFree-form category (e.g. "iPhone", "Smart TV"). Legacy alias device is still accepted.
departmentstringnoFree-form organisational unit, useful in office deployments.
notesstringnoFree-form notes — anything you’d otherwise put in a comment.

Importing blocklists

Blocklists are the domain sources that profiles consume. purge.cc maintains a curated catalogue at https://lists.purge.cc/ (plain-domain lists) and https://rules.purge.cc/ (AdGuard-syntax rule lists). You can also point at any third-party URL you trust.

TOML — blocklists.d/curated.toml:

/etc/purge-warden/blocklists.d/curated.toml
toml
[[blocklists]]
id = "privacy-ads"
display_name = "Privacy: ads"
url = "https://lists.purge.cc/privacy/ads.txt"
format = "domains"
update_interval_hours = 12

[[blocklists]]
id = "oisd-big"
display_name = "OISD Big"
url = "https://big.oisd.nl/"
format = "adguard"
update_interval_hours = 6
CLI equivalent:

warden blocklist add privacy-ads \
  --url "https://lists.purge.cc/privacy/ads.txt" \
  --format domains \
  --update-interval-hours 12

warden blocklist add oisd-big \
  --url "https://big.oisd.nl/" \
  --format adguard \
  --update-interval-hours 6

format is one of domains (one hostname per line — typical for Pi-hole adlists), adguard (AdGuard rule syntax with || / @@ / $important), or hosts (/etc/hosts-style with leading IPs).

Force a fresh download right after adding:

sudo warden update
Info
for private lists that need an Authorization: Bearer … header, store the secret in /etc/purge-warden/secrets.toml (mode 0600) and reference it via auth_token_ref = "<key>". The daemon refuses to start if secrets.toml is world-readable.

The first refresh shows one progress line per list, plus an aggregate count of unique domains parsed:

warden · update
$ warden update no running daemon found, performing foreground list download... catalog fetched (16 lists available) 2026-05-04T08:30:01.114Z INFO purge_shield::lists::manager: list downloaded and parsed source="privacy-ads" added=58234 2026-05-04T08:30:02.821Z INFO purge_shield::lists::manager: list downloaded and parsed source="oisd-big" added=261847 2026-05-04T08:30:03.402Z INFO purge_shield::lists::manager: list downloaded and parsed source="security-malicious" added=14952 downloaded and merged: 312088 unique domains

Importing profiles and groups

Profiles are the filtering bundles; groups apply a profile to a named set of devices.

TOML — profiles.d/kids.toml and groups.d/family.toml:

/etc/purge-warden/profiles.d/kids.toml
toml
[profiles.kids]
display_name = "Kids — daytime"
blocklists = ["privacy-ads", "security-malicious", "categories-social"]
block_response = "nxdomain"
/etc/purge-warden/groups.d/family.toml
toml
[[groups]]
id = "kids"
display_name = "Kids in the household"
profile = "kids"
priority = 10
devices = ["daughter-iphone", "son-switch"]
CLI equivalent:

warden profile set kids \
  --display-name "Kids — daytime" \
  --blocklists "privacy-ads,security-malicious,categories-social" \
  --block-response nxdomain \
  --into profiles.d/kids.toml

warden group add kids \
  --profile kids --priority 10 \
  --devices "daughter-iphone,son-switch" \
  --display-name "Kids in the household" \
  --into groups.d/family.toml
Warning
every blocklists id you reference must exist in some [[blocklists]] entry first. The validator rejects dangling references at lint time — import the blocklist files before you import a profile that uses them.

To inspect a profile after import — useful for confirming the lists, deny and allow rules resolved as you expected:

warden · profile show kids
$ warden profile show kids profile: kids block_all: false lists: - privacy-ads - oisd-big deny rules: - tiktok.com - youtube.com allow rules: - khanacademy.org

Putting it together

A realistic split layout that pulls all the above into one server:

/etc/purge-warden/
├── config.toml              # master with [server] + includes
├── secrets.toml             # mode 0600
├── blocklists.d/
│   └── curated.toml         # privacy-ads, oisd-big
├── profiles.d/
│   ├── default.toml         # [profiles.default]
│   └── kids.toml            # [profiles.kids]
├── groups.d/
│   └── family.toml          # [[groups]] kids
└── devices.d/
    └── family.toml          # [[devices]] edo-laptop, daughter-iphone, …

The master only declares globals and the include globs:

/etc/purge-warden/config.toml
toml
schema_version = 1

[server]
listen = "0.0.0.0:53"
default_profile = "default"
allow_from = ["192.168.1.0/24", "127.0.0.0/8"]

includes = [
  "blocklists.d/*.toml",
  "profiles.d/*.toml",
  "groups.d/*.toml",
  "devices.d/*.toml",
]
Apply the whole tree at once:

warden config lint                       # validate everything that will be loaded
sudo systemctl reload purge-warden       # hot reload, no downtime
sudo warden update                       # download every blocklist now

After the reload, warden config show prints the merged view — every include is flattened into a single TOML preview, which is what the daemon actually runs:

warden · config show
$ warden config show # /var/lib/purge-warden/config.toml schema_version = 1 [server] listen = "0.0.0.0:53" default_profile = "default" allow_from = ["192.168.1.0/24", "127.0.0.0/8"] [[blocklists]] id = "privacy-ads" display_name = "Privacy: ads" url = "https://lists.purge.cc/privacy-ads.txt" format = "domains" [profiles.kids] display_name = "Kids" blocklists = ["privacy-ads", "oisd-big"] deny = ["tiktok.com"] allow = ["khanacademy.org"]

Verifying everything imported correctly

After an import, run this checklist top-to-bottom. Each command shows you a different angle on the merged config.

1. Does the merged config parse and pass every cross-reference check?
bash
warden config lint

# 2. Show the resolved view (defaults expanded, references followed)
warden config show --resolved

# 3. Are all the blocklists present and downloaded?
warden blocklist list

# 4. Are all the devices visible to the daemon?
warden device list
warden device list --live          # add per-device live query counters

# 5. Do groups, subnets, profiles look right?
warden group list
warden subnet list
warden profile list

# 6. For a given client IP, what profile does the daemon pick?
warden resolve 192.168.1.10

# 7. Audit trail of every recent mutation, boot, and reload
warden audit tail -n 20
After step 4 (warden device list), you should see one line per configured device, with the IP, MAC, and the profile the daemon assigned to it:

warden · device list
$ warden device list configured devices (4): edo-laptop "Edo's laptop" ip=192.168.1.20 mac=02:00:00:11:22:33 profile=default kids-iphone "Kids iPhone" ip=192.168.1.41 mac=02:00:00:aa:bb:cc profile=kids groups=[family] nas "Synology NAS" ip=192.168.1.10 mac=02:00:00:de:ad:01 profile=default guest-tv "Guest room TV" ip=192.168.1.55 mac=02:00:00:ca:fe:42 profile=blocked groups=[guest]

A few sanity probes against live DNS confirm the rules are actually in effect:

dig @127.0.0.1 google.com                # → normal A record
dig @127.0.0.1 doubleclick.net           # → 0.0.0.0 if 'privacy-ads' is loaded
Tip
if warden resolve <ip> returns the wrong profile, read the level: line in its output. It tells you which resolver tier matched (device direct, schedule override, group, subnet longest-prefix, or default). Mismatches almost always come from an unexpected device entry or a subnet priority collision — both surfaced verbatim in that output.

Where to go next

  • Field reference for every entity in the examples above: ./CONFIG_GUIDE.public.md.
  • Migrating a Pi-hole or AdGuard Home install: (TODO: link migration-from-pihole)
  • Curated blocklist catalogue: https://lists.purge.cc/index.json and https://rules.purge.cc/index.json.
  • Quick start and troubleshooting: ./README.md.