The terminal has moved way beyond local dev. It’s where applications get deployed, infrastructure gets automated, CI pipelines run, and, increasingly, where agents take action on your behalf. That raises the bar for what a CLI has to be. A thin wrapper around an API isn't enough. It needs to be predictable, scriptable, discoverable, and easy for both people and agents.
That's been the focus of the bunny.net CLI since it launched. The first release shipped with full Database support, letting you create databases and query them without leaving the terminal. A few weeks later, Edge Scripting landed, so you could scaffold, deploy, and manage serverless functions the same way. Since then, we've kept folding more of the platform into the same workflow.
Today, Bunny DNS joins it.
Bunny DNS is our authoritative DNS hosting, and the routing engine behind our global CDN. It hosts your zones and records like any DNS provider, but it's also scriptable, so you can run your own code at query time to decide how a name answers, instead of just returning static records. The CLI can now manage both your records and the scripts that answer them.
A two-minute tour
The goal we set ourselves for this release was simple: take a domain from nothing to live on bunny.net, pointed at something real, without opening the dashboard. Here's the whole arc.
# Create the zone bunny dns zones add example.com ✓ Created DNS zone example.com (ID: 819216). Now update your nameservers at <your registrar> to: kiki.bunny.net coco.bunny.net # Verify nameservers are detected bunny dns zones ns example.com ✓ Nameservers detected and pointing to Bunny DNS for example.com. # Point the apex at your origin, and www straight at a pull zone bunny dns records add example.com '@' A 198.51.100.1 bunny dns records add example.com www PULLZONE my-site
Once your registrar picks up the new nameservers, the domain is live: the apex resolved directly, and www served from your pull zone across the bunny.net network. Your DNS and CDN are configured together in four commands.
That's the scripted path. When you'd rather explore, run bunny dns records add example.com with no type and the CLI walks you through it, including presets that set up a whole group of records at once:
✔ What would you like to add? › A preset (email providers, verification, security)
? Choose a preset: › - Use arrow-keys. Return to submit.
❯ Google Workspace Email
Gmail-hosted email: MX, SPF, optional DKIM and DMARC.
Microsoft 365 (Outlook) Email
Zoho Mail Email
Mailgun Email
Resend Email
Proton Mail Email
Bluesky handle Verification
DMARC policy Email security
CAA (restrict certificate issuance) Email security
No email (anti-spoofing) Email security
✔ Google DKIM TXT value (Admin console) (blank to skip) …
✔ DMARC report email (blank to skip) …
This will add 2 record(s) to <yourdomain>:
Type Name Value Priority
MX @ smtp.google.com 1
TXT @ v=spf1 include:_spf.google.com ~all
? Add these 2 record(s)? › (Y/n)
✓ Applied Google Workspace to <yourdomain>: 2 record(s) added.
Choosing Google Workspace fills in the MX and SPF records for you, then asks for the two things it can't infer: an optional DKIM value from your Google admin console, and an optional DMARC report address. DKIM and DMARC are the parts of email security people most often skip, because they're easy to get wrong when configured by hand, so folding them into the preset means new domains start with sender authentication already in place.
The CLI shows exactly which records it will create and waits for a yes before writing any of them. Presets cover the other major mail providers too, including Microsoft 365, Zoho, Mailgun, Resend, and Proton Mail, alongside domain verification and standalone security records like DMARC and CAA.
Presets are the kind of thing a human reaches for while setting a domain up. An agent can skip straight past them with explicit flags.
DNS was always meant to be automated
DNS sits at the start of almost every project. Before a request reaches your application, your CDN, your edge script, or your origin, DNS decides where it goes. That makes it one of the most important parts of your infrastructure, and one of the most useful things to automate.
Creating a zone for a new project, adding records as part of a deployment, pointing a hostname at a script to route requests, exporting a zone before you change it, tearing down temporary records when a preview environment is destroyed. Each is part of a workflow rather than a one-off dashboard task, and now they're all commands you can script, run in CI, or hand to an agent.
And automating it doesn't run up a usage bill. Since Bunny DNS dropped per-query pricing, DNS hosting is now included for up to 500 domains per account, with no per-query charges and no query limits. No matter how often your pipelines and agents hit it, the queries are free.
Here are the DNS commands the CLI now supports.
Zones
You've already seen zones add, which creates a zone and prints the bunny.net nameservers to set at your registrar. The rest of the lifecycle is where you'd expect:
bunny dns zones list bunny dns zones show example.com bunny dns zones remove example.com
A zone is identified by its domain name or its numeric ID, so example.com and the zone ID are interchangeable wherever a command takes one.
Link a directory to a zone
Passing the domain on every command gets repetitive. Link the working directory to a zone once and the rest of the dns commands know which zone you mean:
bunny dns zones link example.com
This writes a small .bunny/dns.json manifest into your project. From then on, zone and record commands run against the linked zone without naming it. It's the same pattern as bunny db link, bunny scripts link, and soon bunny storage link, and as with those, the manifest is per-developer state, so add .bunny/ to your .gitignore. When a command falls back to the interactive picker, it offers to link the directory for you.
Records
Records live inside a zone. records add takes the name, type, and value, with the zone apex written as '@':
bunny dns records add example.com api A 198.51.100.1 bunny dns records add example.com '@' MX mail.example.com 10 bunny dns records list example.com
Run bunny dns records add with the type left off and it walks you through an interactive wizard instead. Bunny DNS supports the record types you'd expect alongside a few bunny-specific ones, including PULLZONE to point a name straight at a pull zone and SCRIPT to answer it with Scriptable DNS.
Updating and removing records works by record ID, which you can read from records list, or you can leave the ID off and pick from a list. Only the fields you pass are changed:
bunny dns records update example.com 123 --value 198.51.100.2 bunny dns records remove example.com 123
Migrate a zone with BIND files
Moving a domain onto Bunny DNS, or taking a backup before a big change, comes down to two commands. Export writes a standard BIND zone file, and import reads one back in:
bunny dns records export example.com --save bunny dns records import example.com ./example.com.zone
--save writes example.com.zone alongside you. Omit it to print the zone to stdout and pipe it wherever you like.
DNSSEC and zone settings
Turn on DNSSEC and the CLI prints the DS record to hand to your registrar:
bunny dns zones dnssec enable example.com
Bunny DNS implements DNSSEC without exposing your zone's structure, so this is one you can switch on without the usual hesitation. Query logging and per-zone statistics are a command away too, with bunny dns zones logging and bunny dns zones stats.
Scriptable DNS from the CLI
Scriptable DNS runs your code at query time to decide how a name should answer, which is how you build geo routing, weighted answers, failover, and closest-region selection. It's a separate runtime from Edge Scripts, so it has its own command group under bunny dns scripts.
The flow mirrors Edge Scripting. Scaffold a project from an example, deploy the script, and attach it to a hostname:
bunny dns scripts init geo-router --example geo --deploy cd geo-router bunny dns scripts deploy handleQuery.js bunny dns scripts attach example.com geo --script <id>
init drops a handleQuery entry file. The starters cover the common routing shapes (geo, closest, weighted, failover, and pullzone), plus an empty one to begin from scratch:
/// <reference types="@bunny.net/scriptable-dns-types" /> /** @param {DnsRequest} query */ export default function handleQuery(query) { const { geoLocation, serverZone } = query.request; return new TxtRecord( `country=${geoLocation.country} zone=${serverZone} asn=${geoLocation.asn}`, 30, ); }
That reference line at the top is new too: @bunny.net/scriptable-dns-types ships ambient types for the Scriptable DNS runtime, so your editor autocompletes queries, records, and helpers even in plain JavaScript. No more guessing, no docs tab.
A script does nothing to your DNS until it backs a record, which is what attach handles: it adds a SCRIPT record so geo.example.com is answered by your code.
Because that record changes how a name resolves, attach always asks before it writes, and attaching at the apex prints a louder warning and lists anything already there first. In a pipeline, where there's no one to confirm, it refuses to write unless you pass --force.
That turns DNS into something you build with. Records in a table become routing logic that lives alongside the rest of your infrastructure.
Works with your agent and CI
An agent driving a tool needs three things: to discover what it can do, to call it and get a predictable result, and to recover when something goes wrong. The DNS commands are built for all three, the same way the rest of the CLI is.
Discovery comes from a consistent command structure and the skill files we ship alongside the CLI. An agent doesn't have to reverse-engineer the dashboard. It reads the skill, finds that zones, records, and scripts each follow the same shape, and knows that bunny dns <command> --help will spell out the flags.
Reliable calls come from being flags-first and deterministic. Pass the domain, the record, and the values and you get the same result every time, with no prompt waiting in the way. Linked manifests mean an agent isn't juggling zone IDs in its context, and --output json turns any command into something it can parse rather than scrape.
Recovery comes from how the CLI fails. Errors are written in plain language with a hint at the fix, exit codes are meaningful, and under --output json the error comes back as structured JSON too. An agent can tell a fixable mistake from an internal one and respond, instead of retrying blindly. Prompts and pickers are TTY-aware so they step out of the way in CI, and destructive commands like records remove and scripts attach take --force for the same reason.
The result is that the commands you run are the commands your automation runs, and the commands your agent runs. No separate integration to maintain, no protocol to configure.
One CLI for the platform
Every release brings another part of bunny.net into the workflow developers already use:
- Databases
- Edge Scripts
- DNS and Scriptable DNS
- Agentic workflows across all of them
Coming up next are Magic Containers and Storage.
Get started:
npm install -g @bunny.net/cli bunny login bunny dns zones add example.com
The CLI is open source and built in public. If something's broken, open an issue. If you've built something with it, come and tell us about it in Discord.

