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

Custom rules

Under review — wording may still change.

Rules you write yourself — a file of many, or a single line in the config.

What this step does

Imported lists are other people’s judgement calls. Sooner or later you need your own: unblock the domain a list got wrong, block the one every list missed, keep a short household deny-list no feed will ever carry.

Warden gives you two mechanisms for that. Choosing between them is a question of volume, not of power.

Two ways to write your own rule

Custom listAdmin rule
What it isa file of many rules, packs/<id>.txta single rule declared in the config
Declared as[[custom_lists]][[admin_rules]]
Grammartwo forms onlywildcards, regex, $important
Reaches a profile byprofiles.<id>.custom_lists = ["<id>"]a reference from a profile, device, group or subnet
Created fromthe Custom Lists tab, or the file itselfthe allow / deny verbs, the query log, the Rules tab

Both end up in the same place: a profile’s allow_domains / deny_domains, the exact-match sets the engine probes on every query. A custom list reaches only those two sets — nothing in a pack goes anywhere else. An admin rule can land in either seat: a plain ||domain^ joins the same sets, while a wildcard, a regex or a $important modifier goes into the advanced-rule vector, scanned line by line on every query.

A custom list is not the weaker mechanism, then. It is the bulk one, and its grammar is narrow because the bulk path has to stay a hash lookup. src/profiles/profile.rsbuild_v1().

Custom lists, end to end

The file

A custom list is one text file at <config_root>/packs/<id>.txt. config_root is the directory holding your master config — not that of the include fragment that declares the entity. With the default config at /var/lib/purge-warden/config.toml and a list id of minecraft:

text
/var/lib/purge-warden/packs/minecraft.txt

The path is derived from the id and cannot be configured — a custom list has no file field. That makes a traversal path, an absolute path, and two entries sharing one file unrepresentable. A symlink in that slot is opened O_NOFOLLOW and refused. src/config/custom_list/mod.rspack_path().

The grammar

This is the part that costs people an afternoon. A pack accepts exactly two rule forms. Not AdGuard syntax — a two-form subset of it — and not a hosts file either.

LineVerdict
||ads.example.com^deny
@@||cdn.example.com^allow
# a note to yourself, or a blank lineignored
ads.example.comrefused — bare domain, no prefix, no terminator
0.0.0.0 ads.example.comrefused — hosts syntax
||*.example.com^refused — wildcard
/^ads[0-9]+\.example\.com$/refused — regex
||example.com^$importantrefused — modifier

Surrounding whitespace is tolerated. The domain body must be plain ASCII letters, digits and hyphens, is lowercased on the way in, and is capped at 253 bytes overall and 63 per label, with no hyphen at a label edge.

A refused line does not fail the file: it is skipped and counted, and the rest of the pack loads. The refusals exist for the reason above — a wildcard, a regex or a $ modifier would have to live in the advanced-rule vector, walked linearly for every query. src/config/custom_list/grammar.rsparse_pack_line().

Declare it, then mount it

The file on disk does nothing until a [[custom_lists]] entry names it and a profile mounts it.

/var/lib/purge-warden/config.toml
toml
[[custom_lists]]
id = "minecraft"
display_name = "Minecraft servers"

[profiles.kids]
custom_lists = ["minecraft"]
lists = { ads = "deny" }

custom_lists must come before lists in that table: TOML cannot emit a bare value after a table has started, so the other order is a parse error, not a style choice.

A profile mounting an id nobody declared is refused at load — profile "kids" references custom_list "minecraft" which is not defined.

There is no CLI verb for custom lists

Not warden custom-list, not warden profile custom-lists. Neither exists. You create and grow a custom list in the TUI, or write the file yourself and declare it by hand. warden config edit opens the config in $EDITOR and validates what you save, so it reaches the [[custom_lists]] block and the mount — it does not open the pack file.

Size, and what deleting removes

[custom_list_limits] max_file_bytes caps one pack, default 1 MiB. Loading is all-or-nothing: one unreadable pack fails the whole config load, though every failure is reported at once.

Deleting the declaration removes the entity, not the file — the pack stays on disk and reappears the moment you declare that id again.

Admin rules

From the CLI

warden profile allow <profile> <domain> synthesises @@||domain^ as a new [[admin_rules]] row and references it from that profile, in one step. deny does the same for ||domain^. warden subnet profile-allow / profile-deny are the subnet-shaped pair.

A subnet rule is not subnet-local
These do not write a rule that stops at the subnet. They write onto the profile that subnet references, so the rule also reaches every other subnet, group and device pointing at that profile. To confine it, give the subnet a profile of its own first.

From the query log

The fast path: you see a name in the log and act on it without leaving the screen.

  1. Open the query log — 2, or g q.
  2. Move to the row you want and press Enter.
  3. The action is inferred from the row, never chosen. BLOCKED offers Allow. ALLOWED, CACHED and STALE offer Deny. A LOCAL, REFUSED or HINFO row offers nothing — no filter rule produced those, so there is nothing to invert, and you get a footer message instead of a modal.
  4. Pick the scope: Device, Profile, Group, Subnet or Default.
  5. Confirm. The confirmation is tiered by blast radius — a device is one keypress, a profile, group or subnet makes you type its id, the default makes you type the literal DEFAULT.

What lands on disk is an [[admin_rules]] row holding ||domain^ or @@||domain^, with an id generated as auto-<action>-<8hex>, plus a reference to it on the entity you scoped to. The daemon reloads and the write is audited as rule.add, surface = "tui".

The modal captures the domain and client at the instant you press Enter, so a log scrolling underneath you cannot change what you are about to write.

Taking one back

Neither removal path takes a rule id:

  • warden profile allow|deny <profile> <domain> --remove inverts what you wrote: it drops the reference, and cascades the row drop when nothing else still names that id.
  • warden rule undo pops the most recent [[admin_rules]] row and cascades the reference drop across every profile, device, group and subnet that named it.

To delete one specific older rule, use the Rules tab.

Which one wins

Warden evaluates a query in this order, stopping at the first step that produces a verdict:

  1. block_all — BLOCK, unless an admin allow matches.
  2. $important allow — FORWARD.
  3. $important deny — BLOCK.
  4. A normal allow rule, or a custom-list allow — FORWARD.
  5. A normal deny rule, or a custom-list deny — BLOCK.
  6. An allow-direction list — FORWARD.
  7. A deny-direction list — BLOCK.
  8. Nothing matched — FORWARD.

The consequence you need: inside one pack an allow beats a deny for the same domain, and either beats any list verdict on it. Steps 2 and 3 are out of a pack’s reach — only an admin rule carries $important.

That is the short version, scoped to the rules on this page. Profiles own the full chain — how the profile itself gets chosen, and what block_all and local_records do to the order before any of this runs: decision precedence.

CLI reference

VerbWhat it does
warden profile allow <profile-id> <domain>new @@||domain^ row plus the reference on that profile. --id <s> names it, --remove inverts, --into <path> writes to a fragment
warden profile deny <profile-id> <domain>the same for ||domain^
warden profile admin-rule add <profile-id> <rule-id>make a profile enforce a row that already exists. Does not create one
warden profile admin-rule remove <profile-id> <rule-id>drop the reference. The row itself stays
warden subnet profile-allow <subnet-or-cidr> <domain>allow on the profile that subnet references. Same three flags
warden subnet profile-deny <subnet-or-cidr> <domain>the same, blocking
warden rule undopop the last [[admin_rules]] row and cascade the reference drop
warden config editopen the config in $EDITOR, validate on save. Reaches the declaration, not the pack file

No verb on this table creates, mounts or deletes a custom list, and no other verb does either.

In the TUI

Launch warden dashboard, then 4 for Filters. g t opens Custom Lists, g u opens Rules.

KeyPaneDoes
aListsCreate a custom list — writes the pack file first, then the [[custom_lists]] block
eListsEdit the display name and description. The pack file is not touched
d or DelListsDelete the declaration. The confirm screen shows how many profiles mount it and how many rules it holds
mListsMount or unmount on profiles. Space toggles, Enter applies, Esc cancels
aRulesAdd one rule to the selected list’s pack. Appends, order-preserving, and a duplicate is a no-op
d or DelRulesRemove the rule under the cursor
, or h lbothSwitch pane
arrows, Home/End, PgUp/PgDnbothMove. The Rules pane follows the Lists cursor with no keystroke of its own

Two things that surprise people:

  • There is no rename. e edits the display name and description only; the id is the file name.
  • d in the Rules pane matches on the domain and ignores direction, so it drops both the allow and the deny form for that domain. On a comment or blank line it does nothing — no domain to match.

h and l move between panes here; j and k are bound to nothing — the TUI has no vim aliases anywhere.

The Rules tab is where admin rules are edited and deleted one at a time. f cycles the action filter, / searches the text, R clears both, Enter edits the focused row, a adds one with no row focused, d goes straight to the delete confirmation.

See also

↑↓ to navigate · Enter to open · Esc to close See all results