Initialize
Turn a bare binary into a running scaffold — system user, directories, and a first config.toml
What this step does
warden init provisions everything a fresh install needs before it can start: the purge-warden system user, the state directories, and a first config.toml. It requires root, and refuses outright if it isn’t: warden init requires root. Run with sudo.
It also seeds three blocklist subscriptions and turns filtering on immediately — a list’s base defaults to deny, and the profile init writes has no override, so it inherits base. No tags key is involved anywhere in the output; the tag mechanism that used to carry this was retired.
sudo warden initInteractive mode
With no --yes, init asks four questions in this order. Each accepts a bare Enter for its default:
| # | Prompt | Default (Enter) |
|---|---|---|
| 1 | Upstream resolver | Detected from this machine’s /etc/resolv.conf / resolvectl status, plus any entries from an upstreams.toml menu next to the config. Type 0 to enter an address by hand. |
| 2 | default_profile for unmapped sources | default — type none to leave unmapped clients REFUSED instead. |
| 3 | Blocklist subscriptions | The three defaults — see The three seeded lists. |
| 4 | Allowed client networks ([server].allow_from) | RFC 1918 + loopback: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8. |
Nothing writes upstreams.toml for you — create it by hand next to the config if you want a curated menu instead of the free-typed entry.
Non-interactive mode (--yes)
Scripted installs skip every prompt:
sudo warden init --yes--yes accepts the baked-in defaults for the profile (default), the three lists, and allow_from — but the upstream resolver still has to come from somewhere. With no --upstream flag, --yes adopts whatever this machine’s own resolver configuration already points at, and prints what it chose so an install transcript isn’t silent about it:
upstream: adopting 10.10.1.1:53 (detected on this machine)
init detects what this machine already uses and offers it (interactively, as menu entry 1; under --yes, adopted automatically), and refuses rather than inventing a value if nothing usable is found. See Troubleshooting for both refusal messages.What it writes
A fresh config.toml looks like this (list URLs and the admin token line omitted for space; see What init does not write below):
schema_version = 3
[server]
listen = "0.0.0.0:53"
allow_from = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8"]
log_level = "info"
default_profile = "default"
[upstream]
mode = "plain"
servers = ["10.10.1.1:53"]
timeout_ms = 5000
[lists]
sources = []
update_interval_secs = 43200
[[blocklists]]
id = "security-malicious"
display_name = "Security: malicious"
url = "https://lists.purge.cc/malicious.txt"
[[blocklists]]
id = "privacy-ads"
display_name = "Privacy: ads"
url = "https://lists.purge.cc/ads.txt"
[[blocklists]]
id = "privacy-tracking"
display_name = "Privacy: tracking"
url = "https://lists.purge.cc/tracking.txt"
[profiles.default]
display_name = "Default household profile"
[cache]
max_entries = 10000
# max_ttl_secs / min_ttl_secs / negative_ttl_secs also set, to sane defaults
[socket]
path = "/run/purge-warden/control.sock"
# [api]
# enabled = false — commented out; enable and set a token to use the HTTP API[lists].sources stays empty on purpose — subscriptions live as [[blocklists]] entities, not as slugs in that legacy channel.
The three seeded lists
--yes, or a bare Enter at the subscription prompt, subscribes these three catalog slugs. The id an entity renders as is the slug with / replaced by -:
| Catalog slug | Entity id | URL |
|---|---|---|
security/malicious | security-malicious | https://lists.purge.cc/malicious.txt |
privacy/ads | privacy-ads | https://lists.purge.cc/ads.txt |
privacy/tracking | privacy-tracking | https://lists.purge.cc/tracking.txt |
Override the set with --lists <csv> of catalog slugs — raw URLs are refused (raw URLs are not accepted by warden init); add a URL-backed source after init with warden blocklist add <id> --url <url>. Browse the full catalog with warden lists catalog.
Filtering starts the moment the daemon starts, for all three: each [[blocklists]] row’s base defaults to deny, and [profiles.default] carries no override, so it inherits base. Nothing attaches a list to the profile — the direction comes entirely from that default.
The default-profile question
--yes hardcodes default_profile = "default". Interactively, a bare Enter also accepts default; typing none writes no [profiles.default] block at all and leaves default_profile commented out in [server] — which means a source that matches no device and no subnet is answered REFUSED, not filtered by some fallback profile.
What init does not write
Only commented examples — no live entries:
- No
[[devices]], no[[groups]], no[[subnets]]. - No
[[custom_lists]], no[custom_list_limits], and nopacks/directory on disk.
The commented [[devices]] example still shows a tags = ["mobile", "personal"] line — that’s a stale scaffold comment; Device has no tags field on current warden. Don’t uncomment it as written.
CLI reference
| Flag | Argument | Meaning |
|---|---|---|
--yes | (bool) | Skip every prompt, accepting the baked-in defaults for profile, lists and allow_from. |
--lists | <slug>[,...] | Comma-separated catalog slugs to subscribe, overriding the three defaults. |
--force | (bool) | Overwrite an existing config at the target path; see Re-running init. |
--upstream | <addr:port>[,...] | Comma-separated upstream resolvers, plain DNS. Wins over both detection and the prompt. |
--upstream-catalog | <path> | Path to the upstreams.toml menu file. Defaults to upstreams.toml next to the target config. |
--allow-from | <cidr>[,...] | Comma-separated CIDRs for [server].allow_from. Must be non-empty. |
--listen | <addr:port> | Bind address for [server].listen. Defaults to 0.0.0.0:53. |
--cluster-secondary | (bool) | Scaffold this node as a cluster secondary; requires --peer. See Cluster secondary. |
--peer | <url> | Primary’s API base URL, e.g. https://10.10.1.94:8053. Only meaningful with --cluster-secondary. |
--install-manpages | (bool) | Also generate manpages into /usr/local/share/man/man1/. |
--man-dir | <path> | Target directory for --install-manpages. |
--config | <path> | Root-level flag (precedes init, not after it) — moves the whole layout; see What it creates. |
What it creates
With no --config, warden init provisions the historical layout:
| What | Where |
|---|---|
| System user | purge-warden — no login shell, no home directory |
| State root | /var/lib/purge-warden/ |
| Downloaded lists | /var/lib/purge-warden/lists/ |
| Stats snapshots | /var/lib/purge-warden/data/ |
| Control socket + PID | /run/purge-warden/ |
| Master config | /var/lib/purge-warden/config.toml, mode 0640 — written only if missing, or if you pass --force |
Pass --config <path> and every one of those moves with it.
Re-running init (--force)
Without --force, init refuses before touching anything if the target config already exists:
config already exists: /var/lib/purge-warden/config.toml. Pass --force to overwrite (the existing file will be renamed with a .pre-init-<ts> suffix).
With --force, the existing file is renamed aside first — config.toml becomes config.toml.pre-init-20260731T140502Z — so a mistaken re-init is a rename away from recovery, not a lost file. That rename protects the config file only; it does not protect anything else you’d want to compare or restore selectively. Run warden config backup first — see Backup & restore.
Cluster secondary
--cluster-secondary (with --peer <primary-api-url>) writes only node-local sections — no [upstream], no [[blocklists]], no [profiles.*]; those arrive from the primary. The result is deliberately not bootable until warden cluster join runs.
After init
init writes the config but creates no IPC token — every mutating or admin command refuses without one. The command prints the next steps itself:
If [server].listen binds a port below 1024 (the scaffold default, 0.0.0.0:53, does), the daemon needs CAP_NET_BIND_SERVICE to bind it:
setcap cap_net_bind_service=+ep /usr/local/bin/warden— or run the daemon from the packaged systemd unit instead, which grants the same capability via AmbientCapabilities= and needs no setcap step. Run the daemon from the packaged unit and the capability comes from AmbientCapabilities= instead.
Next: Import lists.
In the TUI
warden init is a root CLI command — there is no TUI equivalent, and nothing in the TUI runs it for you. Once it has run and the daemon is started, warden dashboard opens the TUI, and Configuration (5, or g e) is where the rest of setup continues.
Troubleshooting
no upstream resolver configured — init --yes refuses outright. Nothing usable was found on this machine. Pass one explicitly:
sudo warden init --yes --upstream 192.0.2.53:53the only resolver(s) this machine uses are warden itself (…) — a different refusal, same command. This is the steady state when a working install is re-initialized: the network already points at warden, so adopting the detected address would make warden query itself. Pass --upstream with a resolver outside this host.
config already exists: … Expected — see Re-running init. Add --force, or point --config at a fresh path if you meant to keep the old one.
--lists rejects an entry with “raw URLs are not accepted”. --lists takes catalog slugs only. Subscribe by slug, then add a custom URL source afterwards with warden blocklist add <id> --url <url>.
A mutating command fails after init with a “no token” / authentication error. Expected — init never creates one. Run warden token generate as shown in After init.
For anything not listed here, warden config lint names the problem and the line it is on.
See also
- Import lists — the next step in the base path.
- Basic configuration — where the config lives, and how to validate and reload it after editing.
- Backup & restore —
warden config backup, before a--forcere-init. - CLI reference — config-path discovery order and the rest of the
warden configsurface.