nextdnsctl
Manage NextDNS profiles from the command line, or declaratively from a file you keep in git.
Disclaimer: This is an unofficial tool, not affiliated with NextDNS. Built by a user, for users.
nextdnsctl pull # write your profiles to nextdns.yaml
$EDITOR nextdns.yaml # change what you want
nextdnsctl plan # see exactly what would change
nextdnsctl apply # make NextDNS match the file
Features
- Declarative profiles: denylist, allowlist, security, privacy blocklists, parental
control, settings and rewrites in one YAML file, with
pull/plan/apply. - One atomic request for most changes, so a 2 000-domain list doesn't take 2 000 API calls (and if anything is invalid, nothing is changed).
- Strict list imports in plain, hosts and adblock format: a line that can't be represented exactly is an error with its line number, never a guess.
- Quick edits without a file:
denylist add,allowlist remove,rewrites add, … why: find out which blocklist or setting blocked a domain, and allow it in one step.- Paced to NextDNS's rate limits,
--jsonoutput and meaningful exit codes for scripts.
Installation
pip install nextdnsctl # or: pipx install nextdnsctl / uv tool install nextdnsctl
Requires Python 3.10+. Upgrading from 1.x? See migrating to 2.0.
Authentication
Find your API key at the bottom of https://my.nextdns.io/account, then:
nextdnsctl auth login # prompts without echoing; or: pbpaste | nextdnsctl auth login
nextdnsctl auth status # checks the key works
The key is stored in ~/.config/nextdnsctl/config.json, readable only by you. The
NEXTDNS_API_KEY environment variable takes precedence (handy in CI).
Declarative profiles
nextdnsctl pull writes every profile (or the ones you name) to nextdns.yaml:
version: 1
profiles:
Home:
id: abc123 # pins the profile; the key above is then its name
denylist:
domains:
- bad.example
- { domain: tracker.example, active: false }
sources:
- url: https://example.com/hosts.txt
format: hosts
- file: lists/extra.txt
allowlist: [good.example]
security:
nrd: true
tlds: [zip, mov]
privacy:
blocklists: [nextdns-recommended, oisd]
natives: [apple]
parentalControl:
services: [tiktok]
categories: [gambling]
settings:
logs: { enabled: true, retention: 30d }
rewrites:
- { name: nas.lan, content: 192.168.1.10 }
The rules:
- A section that is present is managed; a section that is absent is left alone. A file
with only
denylist:never touches your security settings. - A managed list is complete. Entries in NextDNS that aren't in the file are removed on
apply; the plan lists removals andapplyasks before making them. - Sources are merged into the list. Inline
domainsplus every source, deduplicated. - Typos are errors. Unknown keys, blocklist ids, service ids and so on are reported with
the file and line (and a "did you mean").
nextdnsctl catalog blocklistslists valid ids. - No secrets. The API key never goes in the file, and
pullnever writes the device setup section (it contains your linked-IP update token). - A profile in the file that doesn't exist yet is created by
apply. YAML anchors work for sharing a section between profiles.
nextdnsctl plan # exit code 0: no changes, 2: changes, 1: error
nextdnsctl plan Home --json # machine-readable
nextdnsctl apply # shows the plan and asks; --yes to skip the question
nextdnsctl -f other.yaml apply Home
apply sends one atomic update per profile when it can. Only a list too large for a single
request (roughly 2 000+ domains) is applied entry by entry, paced to NextDNS's limit of
about 60 writes per minute; the plan tells you how long that will take. If an apply is
interrupted, run it again: it only sends what's still missing.
Importing block lists
Sources (in the file or with denylist import) are read in one of three exact formats:
| Format | Lines | Rejected |
|---|---|---|
plain |
example.com, or a URL like https://example.com/x |
anything else on the line |
hosts |
0.0.0.0 example.com (also 127.0.0.1, ::, ::1) |
other addresses: that's a rewrite, not a block |
adblock |
||example.com^ |
exceptions, $ modifiers, wildcards, paths, cosmetic rules |
The format is detected when every line agrees; a mixed file is an error. Names are
lowercased and internationalized names converted to punycode. An invalid line fails the
import with its line number; pass --skip-invalid (or skip_invalid: true on a source) to
skip such lines with a warning instead. Note that a hosts entry blocks one exact name, while
a NextDNS denylist entry also covers its subdomains.
For very large lists, prefer NextDNS's built-in blocklists (privacy.blocklists) and keep
the denylist for your own overrides.
Quick edits
Choose the profile with -p NAME (name or id) or NEXTDNS_PROFILE:
export NEXTDNS_PROFILE=Home
nextdnsctl denylist list [--active-only | --inactive-only]
nextdnsctl denylist add bad.example https://worse.example/page
nextdnsctl denylist add maybe.example --inactive
nextdnsctl denylist remove bad.example
nextdnsctl denylist import blocklist.txt [--format hosts] [--skip-invalid]
nextdnsctl denylist export [backup.txt]
nextdnsctl denylist clear [--yes]
# the same for allowlist
nextdnsctl rewrites list
nextdnsctl rewrites add nas.lan 192.168.1.10
nextdnsctl rewrites remove nas.lan
nextdnsctl profile list | create NAME | delete NAME
nextdnsctl catalog blocklists | natives | services | categories | tlds
--dry-run shows the change without making it. Each edit is a single request.
Logs and why
nextdnsctl logs --blocked --since 1h
nextdnsctl logs --follow
nextdnsctl why ads.example.com # which blocklist or setting blocked it?
nextdnsctl why ads.example.com --allow # … and add it to the allowlist
These need logging enabled on the profile (settings.logs.enabled; NextDNS creates new
profiles with logging off).
Global options
| Option | |
|---|---|
-p, --profile |
Profile name or id (or NEXTDNS_PROFILE) |
-f, --file |
Profile file (default nextdns.yaml, or NEXTDNS_FILE) |
--json |
Machine-readable output on stdout |
-v / -q |
Show every entry and request / only errors |
--dry-run |
Show what would change without changing anything |
--timeout |
Request timeout in seconds |
Data goes to stdout and everything else (progress, warnings) to stderr, so
nextdnsctl denylist export > backup.txt is safe.
Contributing
Pull requests welcome! See docs/contributing.md. The design and the observed behaviour of the NextDNS API it relies on are in docs/v2-design.md.
License
MIT License - see LICENSE.
Metadata
Release files for nextdnsctl 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nextdnsctl-2.0.0.tar.gz | 52.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nextdnsctl-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 98.9 kB
Release files / nextdnsctl-2.0.0.tar.gz
| Download URL | nextdnsctl-2.0.0.tar.gz |
|---|---|
| Size | 52.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
081ec61c81bc1b55874517be97b35f475cd2d6a1c142a6282dc1eac3f576ec3f
|
|
BLAKE2b-256 checksum How to use checksums |
0fe9af8e545242c0a61814a80480418417abbc56e3b0965de9e647286c686dc6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency logRelease files / nextdnsctl-2.0.0-py3-none-any.whl
| Download URL | nextdnsctl-2.0.0-py3-none-any.whl |
|---|---|
| Size | 46.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6aa7294a1e817d49cb596a4466a897124866178eca2f45fe28227009e8435f6b
|
|
BLAKE2b-256 checksum How to use checksums |
0a7ecaef1721d2b8acbef6f728a3956c9c6f8dfc6854cd3c9fd1fc9b51cdf685
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency log