nextdnsctl
A command-line tool for NextDNS: bulk-edit your denylist and allowlist, import block lists from files or URLs, and back them up.
Disclaimer: This is an unofficial tool, not affiliated with NextDNS. Built by a user, for users.
nextdnsctl auth login
nextdnsctl -p "My Profile" denylist import https://example.com/blocklist.txt
nextdnsctl -p "My Profile" denylist add bad.example worse.example
nextdnsctl -p "My Profile" allowlist remove too-strict.example
nextdnsctl -p "My Profile" denylist export > backup.txt
Features
- Bulk add, remove and import for the denylist and allowlist, from the command line or from a file or URL, in plain, hosts or adblock format.
- Fast and safe: importing 2 000 domains is one API request, not 2 000, and if anything is invalid, nothing is changed. Entries that are already there are skipped.
- Strict imports: a line that can't be represented exactly in NextDNS is an error with its line number, never a guess.
- Export a list for backup, and
--dry-runto preview any change. why: find out which blocklist or setting blocked a domain, and allow it in one step.- Whole profiles as code (new in 2.0): describe denylist, allowlist, security, privacy,
parental control, settings and rewrites in a YAML file, and
plan/applyit.
Installation
# Homebrew (macOS, Linux)
brew tap danielmeint/tap
brew trust danielmeint/tap # recent Homebrew asks you to trust third-party taps
brew install nextdnsctl
# PyPI (Python 3.10+)
pipx install nextdnsctl # or: uv tool install nextdnsctl / pip install nextdnsctl
# Nix
nix run github:danielmeint/nextdnsctl -- --help
Upgrading from 1.x? Your commands and stored API key keep working, some with a deprecation warning; invalid domains are now an error instead of being skipped. 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).
Choosing a profile
Pass the profile's name or id with -p, or set it once for your shell:
nextdnsctl profile list
export NEXTDNS_PROFILE="My Profile"
The examples below assume NEXTDNS_PROFILE is set.
Denylist and allowlist
nextdnsctl denylist list [--active-only | --inactive-only]
nextdnsctl denylist add bad.example https://worse.example/page # a URL is reduced to its host
nextdnsctl denylist add maybe.example --inactive # listed, but not blocked
nextdnsctl denylist remove bad.example
nextdnsctl denylist import blocklist.txt # a file or a URL
nextdnsctl denylist export [backup.txt] # stdout by default
nextdnsctl denylist clear [--yes]
Everything works the same for allowlist. Add --dry-run (before the subcommand) to see
what would change without changing anything:
$ nextdnsctl --dry-run denylist add bad.example evil.example
Profile My Profile (abc123)
denylist: +2 (14 → 16 entries)
+ bad.example
+ evil.example
Writes: 1 atomic update
Dry run: nothing was changed.
add and import skip domains that are already in the list. A domain that is there with
the other active/inactive state is left alone unless you pass --update-existing.
Importing block lists
import reads one of three exact formats, detected automatically when every line agrees:
| Format | Lines | Rejected |
|---|---|---|
plain |
example.com, or a URL like https://example.com/x; # comments |
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^; ! comments |
exceptions, $ modifiers, wildcards, paths, cosmetic rules |
nextdnsctl denylist import https://example.com/hosts.txt
nextdnsctl denylist import mixed.txt --format plain
nextdnsctl denylist import messy.txt --skip-invalid
Names are lowercased and internationalized names converted to punycode. An invalid line
fails the import with its line number, and nothing is changed; --skip-invalid skips such
lines with a warning instead. Note that a hosts entry blocks one exact name, while a NextDNS
denylist entry also covers its subdomains.
As long as the resulting list stays under roughly 2 000 domains, any change to it is a single request. Beyond that, changes are applied entry by entry, paced to NextDNS's limit of about 60 writes per minute: importing 10 000 domains takes about three hours. For lists that size, prefer NextDNS's built-in blocklists (under Privacy) and keep the denylist for your own additions and overrides.
More commands
nextdnsctl why ads.example.com # which blocklist or setting blocked it?
nextdnsctl why ads.example.com --allow # … and add it to the allowlist
nextdnsctl logs --blocked --since 1h
nextdnsctl logs --follow
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
why and logs need logging enabled on the profile (NextDNS creates new profiles with
logging off).
Managing whole profiles from a file
New in 2.0: describe one or more profiles in a YAML file, keep it in git, and let nextdnsctl make NextDNS match it.
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
A file can be as small as one list, or describe everything:
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, read in the formats described above. - 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
If an apply is interrupted, run it again: it only sends what's still missing.
Global options
| Option | |
|---|---|
-p, --profile |
Profile name or id (or NEXTDNS_PROFILE) |
--dry-run |
Show what would change without changing anything |
--json |
Machine-readable output on stdout |
-v / -q |
Show every entry and request / only errors |
-f, --file |
Profile file for pull/plan/apply (default nextdns.yaml, or NEXTDNS_FILE) |
--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.1
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.1.tar.gz | 57.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nextdnsctl-2.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 108.4 kB
Release files / nextdnsctl-2.0.1.tar.gz
| Download URL | nextdnsctl-2.0.1.tar.gz |
|---|---|
| Size | 57.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6e191b2d4ed33f07e816abe01688faa7a6274a228e0b8f1467dad5b894d5deb0
|
|
BLAKE2b-256 checksum How to use checksums |
4d0d419f816e5fdfad2b40a01b61109525231fee7e05f1d54de0e4cfcf7ba666
|
| 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.1-py3-none-any.whl
| Download URL | nextdnsctl-2.0.1-py3-none-any.whl |
|---|---|
| Size | 51.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bd490723cd27bd2987de0dd58d2f5805fbc83c06d5ba2717c13350877357d7b6
|
|
BLAKE2b-256 checksum How to use checksums |
413ec48a8fa5f9a0b60fbe57bd704c29b80801b886eaa6f0650795e2abbb5388
|
| 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