A cross-platform CLI tool for managing Namecheap DNS records.
cargo install --path .cargo install namecheap-clinamecheap auth loginYou are prompted for your API user and key; the key is typed or pasted without being echoed. The credentials are checked against the API before anything is saved, so a mistyped key is never stored.
A Namecheap API key is full control of the account's domains and DNS, so it is
not written to the config file. auth login stores it in the OS keychain —
Keychain on macOS, Credential Manager on Windows, Secret Service on Linux — where
it is encrypted at rest and scoped to your login session. Each profile gets its
own entry, so --profile work and --profile personal stay separate.
The config file then holds no secret at all, which makes it safe to sync or commit:
[profiles.default]
api_user = "your-api-user"
sandbox = falseWhen a key is needed, it is looked for in this order:
| Order | Source | For |
|---|---|---|
| 1 | NAMECHEAP_API_KEY |
CI and one-off overrides — no stored state needed |
| 2 | OS keychain | The normal case, written by auth login |
| 3 | api_key in the config file |
Profiles saved before the keychain was used |
auth status tells you which one answered:
$ namecheap auth status
✓ Authenticated
Profile: default
API User: your-api-user
API Key: from your keychain
Managing stored credentials:
# Move keys out of an existing plaintext config and into the keychain
namecheap auth migrate
# Remove a profile's key from both the keychain and the config file
namecheap auth logout --profile default
# Machines with no keychain (headless Linux): keep it in the config file
namecheap auth login --store fileOn a machine with no keychain, auth login says so and falls back to the config
file rather than failing. The config file is always written 0600, and
auth status warns if it is readable by anyone else or still holds a plaintext key.
export NAMECHEAP_API_USER="your-api-user"
export NAMECHEAP_API_KEY="your-api-key"
export NAMECHEAP_USERNAME="your-username" # Optional, defaults to API user
export NAMECHEAP_CLIENT_IP="your-ip" # Optional, auto-detected
export NAMECHEAP_SANDBOX="true" # Optional, use sandbox API
export NAMECHEAP_BACKUP_DIR="/path/to/dir" # Optional, where zone snapshots are kept
export NAMECHEAP_API_URL="https://..." # Optional, override the API endpointConfiguration is stored in ~/.config/namecheap-cli/config.toml, written 0600:
default_profile = "default"
[profiles.default]
api_user = "your-api-user"
username = "your-username"
sandbox = false
[profiles.sandbox]
api_user = "your-api-user"
sandbox = trueNote there is no api_key — it lives in the keychain. An api_key here still
works for profiles saved before that, and supports environment expansion
(api_key = "${NAMECHEAP_API_KEY}"), but auth migrate will move it.
# Configure credentials interactively
namecheap auth login
# Check authentication status
namecheap auth status
# Show current user info and balance
namecheap auth whoami# List all domains
namecheap domains list
# Get domain info
namecheap domains info example.com
# Check domain availability
namecheap domains check example.com example.org
# Only domains expiring soon, soonest first
namecheap domains list --expiring 30
# Lock a domain against transfer
namecheap domains lock example.com
namecheap domains unlock example.com
# The lock state is shown by `domains info`, not `domains list` — Namecheap's
# listing reports IsLocked="false" for domains that are in fact locked, so the
# lock is read from getRegistrarLock separately
namecheap domains info example.com
# Renew (charges your Namecheap account, so it always confirms)
namecheap domains renew example.com --years 2
# What a TLD costs — the price this account pays, not the list price
namecheap domains pricing com
namecheap domains pricing .io --action renew
namecheap domains pricing com --action transfer --years 1Without a TLD, pricing returns every TLD Namecheap sells, which is a long
list. --action is one of register, renew, transfer or reactivate.
# List DNS records
namecheap dns list example.com
# Filter by type
namecheap dns list example.com -t A
# Add a record
namecheap dns add example.com A @ 1.2.3.4
# Add several values at once (one record per value, written in a single call)
namecheap dns add example.com A @ 185.199.108.153,185.199.109.153,185.199.110.153,185.199.111.153
namecheap dns add example.com AAAA @ 2606:50c0:8000::153,2606:50c0:8001::153
# Add MX record with priority
namecheap dns add example.com MX @ mail.example.com --priority 10
# Set (replace) a record
namecheap dns set example.com A @ 5.6.7.8
# Set replaces the whole set for that type + host
namecheap dns set example.com A @ 185.199.108.153,185.199.109.153
# Remove a record
namecheap dns rm example.com A @
# Remove specific value
namecheap dns rm example.com A @ 1.2.3.4
# Edit the whole zone in your editor
namecheap dns edit example.com
# Export records
namecheap dns export example.com --format json
namecheap dns export example.com --format zone
# Show diff between current and desired
namecheap dns diff example.com records.json
# Import records from a file (sync and import are the same command)
namecheap dns sync example.com records.json
namecheap dns import example.com records.json
namecheap dns import example.com zone.txt
# Make the zone match the file exactly, removing anything not in it
namecheap dns sync example.com records.json --delete
# Clone every record from another domain
namecheap dns clone old-domain.com new-domain.com
# Clone just the records you need (e.g. a GitHub Pages setup)
namecheap dns clone old-domain.com new-domain.com -t A,AAAA
namecheap dns clone old-domain.com new-domain.com -t CNAME --host www
# Make the target an exact mirror, dropping records the source does not have
namecheap dns clone old-domain.com new-domain.com --deleteclone copies records verbatim — same host, value, TTL and priority — then shows
the diff against the target and asks before applying, so you can review it first
(or use --dry-run to only see the diff). Values are not rewritten: anything
naming the source domain (a redirect target, a mail host, a verification token) is
copied as-is and listed as a warning for you to fix afterwards. Narrow the copy
with -t/--host, and add --delete to remove target records the source lacks.
add and set accept a comma-separated list for the types that can legitimately
hold several values at one host — A, AAAA, MX and NS — creating one record
per value in a single zone write. Every other type takes its value verbatim:
TXT, CAA, SRV and the redirect types because a comma can be part of a single
legitimate value, and CNAME and ALIAS because a host can only have one of
them (a comma in those is reported as an invalid hostname).
add skips values already present, so re-running it will not duplicate records;
set replaces every record for that type and host with the values given.
Every command that writes DNS checks its records first and refuses to write anything if they would be invalid, explaining both the cause and the remedy:
$ namecheap dns add example.com A @ 1.2.3.400
✗ Invalid record
A @ 1.2.3.400 1800
Problem: "1.2.3.400" is not a valid IPv4 address — each of the four parts must be a number from 0 to 255
Fix: Use an address such as 185.199.108.153. To point at a hostname instead, use CNAME (on a subdomain) or ALIAS (at the domain root).
Error: Validation error: 1 record(s) would be invalid — nothing was changed
Checks run on your input before any API call, then again on the zone the write would produce, so conflicts with records already in the zone are caught too. What is checked:
| Area | Examples |
|---|---|
| Address values | A holds IPv4 and AAAA holds IPv6 (each suggests the other type when swapped) |
| Targets | CNAME, ALIAS, NS and MX point at a hostname, not an IP or a URL |
| Structured values | CAA flags and tags, SRV's four fields, URL records having a scheme |
| Hosts | Valid labels; a host that already spells out the domain, which would create www.example.com.example.com |
| TTL | Within the 60–60000 seconds Namecheap accepts |
| Zone rules | No CNAME at the root, no CNAME sharing a host with another record, one SPF record per host |
Problems that are legal but probably unintended — a TXT value over the 255-character
single-string limit, a duplicate record, an SRV host without a leading underscore —
are reported as warnings and do not block the write. Validation is scoped to the
hosts you are changing, so a pre-existing problem elsewhere in the zone never blocks
an unrelated update. With --json, issues are emitted as an issues array.
dns edit opens the domain's records in $EDITOR (or $VISUAL, or --editor)
as plain text. Saving applies whatever you changed:
# DNS records for example.com
#
# Edit and save. Change a line to change a record, delete a line to remove
# it, add a line to create one. Lines starting with # are ignored.
#
# Use @ for the domain itself. An MX or SRV record's priority goes at the
# start of its value, as below.
#
# host ttl type value
@ 1800 A 185.199.108.153
@ 1800 A 185.199.109.153
www 3600 CNAME user.github.io.
@ 1800 MX 10 mx1.privateemail.com
@ 1800 TXT v=spf1 include:_spf.google.com ~all
_dmarc 1800 TXT v=DMARC1; p=none
The buffer is the whole zone, so deleting a line deletes that record. What you
save is diffed against what is live, shown, and confirmed before anything is
written — the same flow as sync, with the same validation. If what you save
cannot be read, the problem is reported with its line number and the editor
reopens so no work is lost.
The value is the last field and runs to the end of the line, so TXT records need
no quoting. Semicolons are kept verbatim, which matters for DMARC and DKIM; only
# starts a comment.
This is the same format dns export --format zone produces, and dns sync and
dns diff now accept it as well as JSON:
namecheap dns export example.com --format zone > zone.txt
namecheap dns sync example.com zone.txtpreset apply and dns clone take more than one domain, or read a list from a
file — one domain per line, # comments ignored:
namecheap preset apply github-pages a.com b.com c.com
namecheap dns clone template.com a.com b.com
namecheap preset apply github-pages --domains-from domains.txt
namecheap dns clone template.com --domains-from domains.txtEach domain is diffed and confirmed separately, and dns clone reads the source
zone once however many targets there are. If one domain fails the rest still run;
the failures are listed at the end and the exit code is non-zero.
dns diff --check exits 8 when the live zone differs from the file, so a
pipeline can assert that DNS still matches what is checked in:
namecheap dns export example.com --format zone > expected.txt
# ...later, in CI:
namecheap dns diff example.com expected.txt --checkExport as JSON instead if the domain forwards mail — the JSON form carries the
forwarded mailboxes as well as the records, so --check catches drift in both:
namecheap dns export example.com > expected.json
namecheap dns diff example.com expected.json --checkNamecheap's API replaces a domain's whole record set on every write, so there is
no server-side undo. Before each change, the zone as it stands is saved locally,
and dns rollback puts it back:
# What snapshots exist for a domain
namecheap dns rollback example.com --list
# Undo the last change
namecheap dns rollback example.com
# Go back to a specific snapshot (id from --list)
namecheap dns rollback example.com --at 1755600000$ namecheap dns rollback example.com --list
╭────────────┬─────────────────────────┬─────────┬──────────────╮
│ ID │ Taken │ Records │ Before │
├────────────┼─────────────────────────┼─────────┼──────────────┤
│ 1755600000 │ 2025-08-19 10:40:00 UTC │ 1 │ preset apply │
│ 1755500000 │ 2025-08-18 06:53:20 UTC │ 2 │ dns sync │
╰────────────┴─────────────────────────┴─────────┴──────────────╯
A rollback restores the zone exactly as it was, so records added since the
snapshot are removed as well as changed ones put back. It goes through the same
diff-and-confirm flow as sync, so you see what it will do before agreeing to it,
and --dry-run shows the diff without applying it.
Snapshots include the account's email routing mode, which must be restored
alongside the records or MX records would be silently unpublished. Snapshots
taken before an email-forwarding change also record the forwarded mailboxes, and
rolling back to one of those puts the forwarding back too. Snapshots taken before
an ordinary DNS write do not — a setHosts call cannot change forwarding, and
restoring an empty set would delete mailboxes the change never touched.
The 20 most recent snapshots per domain are kept, under your platform's
application-data directory (override with NAMECHEAP_BACKUP_DIR). Pass
--no-backup to skip taking one.
A snapshot that cannot be written is reported as a warning but does not stop the change you asked for.
# List available presets
namecheap preset list
# Show preset details
namecheap preset show github-pages
# Apply a preset
namecheap preset apply github-pages example.com -V username=myuser
# Remove preset records
namecheap preset remove github-pages example.comApplying an email preset switches the domain's Namecheap email routing mode to MX
and removes any MX records from a previous mail provider, along with a stale
v=spf1 TXT record on the same host — leaving either in place would break mail
delivery. Unrelated records are untouched unless you pass --replace.
Available presets:
github-pages- GitHub Pages hostinggoogle-workspace- Google Workspace emailfastmail- Fastmail emailprotonmail- ProtonMail emailmicrosoft-365- Microsoft 365 emailcloudflare- Cloudflare DNS proxynetlify- Netlify hostingvercel- Vercel hostingsendgrid- SendGrid email sending
# List nameservers
namecheap ns list example.com
# Set custom nameservers
namecheap ns set example.com ns1.cloudflare.com ns2.cloudflare.com
# Reset to Namecheap defaults
namecheap ns reset example.comIf the domain runs its own nameservers, the registry needs to know their
addresses — otherwise nothing can find ns1.example.com in order to ask it
about example.com. That registration is a child nameserver, or glue record.
# Register ns1.example.com at 203.0.113.10
namecheap ns create example.com ns1.example.com 203.0.113.10
# Move it to a new address (the old one is looked up for you)
namecheap ns update example.com ns1.example.com 203.0.113.20
# What the registry has
namecheap ns info example.com ns1.example.com
# Remove it — anything still pointing at it stops resolving, so this confirms
namecheap ns delete example.com ns1.example.comRegistering a child nameserver does not make any domain use it. That is ns set, and the two are independent: you can register ns1.example.com and point
a completely different domain at it.
Privacy is sold as a subscription that is allotted to a domain. The API acts on the subscription's id rather than the domain name, so every command here looks the domain up first — you never have to handle ids yourself.
# Every subscription on the account, and which domain has it
namecheap whoisguard list
# Turn privacy on or off for one domain
namecheap whoisguard on example.com --email you@example.com
namecheap whoisguard off example.com
# Across several domains, or all of them at once
namecheap whoisguard on a.com b.com --email you@example.com
namecheap whoisguard on --all --email you@example.com
namecheap whoisguard on --domains-from domains.txt --email you@example.comenable requires --email: Namecheap needs somewhere to forward the masked
mail. Domains already in the state you asked for are skipped rather than
re-sent, and domains with no subscription are reported rather than ignored.
Turning privacy off publishes your name, address, phone and email in public
WHOIS, where scrapers pick it up quickly. It always lists the affected domains
and confirms, unless you pass -y.
A subscription bought without a domain starts unattached, and privacy cannot be turned on for a domain until one is attached to it.
# Attach the first spare subscription to a domain
namecheap whoisguard allot example.com
# Attach and turn privacy on in one step
namecheap whoisguard allot example.com --email you@example.com
# Attach a specific one (id from `whoisguard list`)
namecheap whoisguard allot example.com --id 123456
# Free a subscription up for a different domain
namecheap whoisguard unallot example.com
namecheap whoisguard unallot --id 123456
# Renew a domain's privacy subscription (charges your account, so it confirms)
namecheap whoisguard renew example.com --years 2Forwarding is not part of the zone. It has its own place in the API, never shows
up in dns list, and Namecheap only delivers it while the domain's mail routing
mode is FWD — which any MX record switches away from. These commands say which
mode the domain is in rather than reporting a write as done and leaving mail on
the floor.
# What the domain forwards
namecheap dns email list example.com
# Forward a mailbox (add is an alias for set)
namecheap dns email set example.com hello you@gmail.com
# Catch everything not matched by another mailbox
namecheap dns email set example.com @ you@gmail.com
# Stop forwarding a mailbox
namecheap dns email rm example.com helloTwo traps this guards against:
- Adding an MX record to a domain that is forwarding mail switches it to MX
routing and the forwarded mailboxes stop being delivered.
dns addanddns setwarn before they do it. - Setting up forwarding on a domain that is already on MX routing appears to
work — the mailboxes are stored — but nothing arrives.
dns email listanddns email setsay so.
Forwarding round-trips through dns export --format json, under an
email_forwarding key, and dns sync and dns diff read it back:
# Capture a domain's records and its forwarding
namecheap dns export example.com > example.com.json
# Put both back
namecheap dns sync example.com example.com.json
# Assert in CI that neither has drifted (exit code 8 if either has)
namecheap dns diff example.com example.com.json --checkMailboxes in the file are added or updated; mailboxes it does not mention are
left alone unless --delete is given, exactly as for records. A file with no
email_forwarding key says nothing about forwarding, so forwarding is left
untouched rather than read as "there should be none". A zone file has no way to
express forwarding at all, so --format zone notes that it exists rather than
pretending it round-trips.
Transferring a domain in to Namecheap. The domain must be unlocked at its current registrar, and you need the auth code (also called an EPP code or transfer key) from them.
# Start a transfer — charges your account, so it always confirms
namecheap transfer start example.com --epp-code ABC123XYZ
# Add more than the default year
namecheap transfer start example.com --epp-code ABC123XYZ --years 2
# Every inbound transfer on the account
namecheap transfer list
# Where one has got to
namecheap transfer status 10
# Nudge a stalled one
namecheap transfer resubmit 10Starting a transfer is an order, not an instant move: the losing registrar still
has to release the domain, which can take up to five days. Transferring out
is not a transfer command — it is namecheap domains unlock plus the auth code,
which you get from Namecheap's web interface.
# List redirects
namecheap redirect list example.com
# Add redirect
namecheap redirect add example.com @ https://www.example.com
# Add permanent (301) redirect
namecheap redirect add example.com old https://new.example.com --permanent
# Add frame/masked redirect
namecheap redirect add example.com masked https://example.com --frame
# Remove redirect
namecheap redirect rm example.com @# Verify DNS records are propagated
namecheap verify example.com
# Verify specific record type
namecheap verify example.com -t A
# Wait for propagation
namecheap verify example.com --wait --timeout 300# Bash
namecheap completions bash > ~/.local/share/bash-completion/completions/namecheap
# Zsh
namecheap completions zsh > ~/.zfunc/_namecheap
# Fish
namecheap completions fish > ~/.config/fish/completions/namecheap.fish
# PowerShell
namecheap completions powershell > namecheap.ps1| Option | Description |
|---|---|
--config <path> |
Path to config file |
-p, --profile <name> |
Profile to use |
--json |
Output as JSON |
--dry-run |
Don't make any changes |
-q, --quiet |
Minimal output |
-v, --verbose |
Verbose output |
-y, --yes |
Skip confirmation prompts |
--no-backup |
Don't snapshot the zone before changing it |
| Code | Description |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Authentication error |
| 3 | Domain not found |
| 4 | Record not found |
| 5 | Validation error |
| 6 | Network error |
| 7 | Verification failed |
| 8 | dns diff --check found differences |
A request is abandoned after 30 seconds, and a connection after 10. Without that, a dropped connection hangs forever and the retry policy never fires — a request that never returns never fails, so there is nothing to retry.
Requests that fail for a transient reason — a timeout, a dropped connection, an
HTTP 5xx, a rate-limit response, or a gateway page in place of the API's XML —
are retried up to three times with an exponentially growing wait (2s, 4s, 8s).
-v reports each retry as it happens.
Only commands that can be repeated safely are retried. Every write this tool
makes sends the complete desired state rather than a delta, so repeating one
converges on the same zone. Everything that spends money is deliberately
excluded — domains renew, whoisguard renew and transfer start — so a lost
response can never become a second charge.
The test suite proves the code does what it thinks it does. It cannot prove Namecheap agrees — that the XML shapes are right, that writes land, and that what the tool reports afterwards matches what the API actually holds.
scripts/live-check.sh closes that gap by driving the built binary against a
real domain and reading every change back:
cargo build
# Read-only. Makes no changes at all.
scripts/live-check.sh example.com
# The safe write phases: records and email forwarding, restored afterwards.
scripts/live-check.sh example.com read dns email
# Everything that does not spend money.
scripts/live-check.sh example.com allall runs these, in order:
| Phase | What it exercises |
|---|---|
read |
Every read-only command. Makes no changes. |
dns |
dns add / set / rm / sync / diff / rollback, and validation refusals |
edit |
dns edit, driven by a scripted stand-in for $EDITOR |
preset |
preset list / show / apply / remove, including variable substitution |
redirect |
redirect add / list / rm across URL, URL301 and FRAME |
email |
The email-forwarding lifecycle, and that it reaches export/diff/sync/rollback |
bulk |
--domains-from, and --all as a dry run |
lock |
The registrar lock, read back from domains info |
glue |
ns create / update / delete / info |
privacy |
whoisguard on / off / allot |
ns |
ns set / reset |
Two more are opt-in and not covered by all, because one needs a second
domain and the other can leave a domain with its registrant details public:
| Phase | Why it is opt-in |
|---|---|
clone |
Writes to a second domain — set NC_CLONE_TARGET |
unallot |
Detaches the privacy subscription and re-attaches it; a failed re-attach leaves the domain with no privacy |
Anything past read rewrites the domain, so it takes a full export as a
baseline first and restores from it on exit — including when a check fails
partway. Write phases make you type the domain name to confirm.
Use a domain that is not serving traffic. The zone is rewritten repeatedly.
Nothing in the script spends money: domains renew, whoisguard renew and
transfer start are deliberately absent, and it refuses to run if asked for
them. Verify those by hand, once, when you actually want to buy something.
Namecheap allows roughly 20 requests a minute and 700 an hour, and a single
dns add costs three of them, so the script paces itself. A full all run is
several hundred calls and takes upwards of half an hour — set
NC_SLEEP=<seconds> to pace it further apart. Going over the limit gets HTML
gateway pages instead of XML, which the tool reports as such and retries.
To use this tool, you need API access enabled on your Namecheap account:
- Go to Profile > Tools > Namecheap API Access
- Enable API Access
- Add your IP address to the whitelist
- Copy your API Key
For testing, you can use the sandbox API by setting sandbox = true in your profile.
CI tracks the CRAP metric — cyclomatic complexity weighted by how much of the function tests actually execute:
CRAP(f) = CC(f)^2 * (1 - coverage(f))^3 + CC(f)
A fully covered function scores its complexity; an untested one scores that cubed against it. The point is to find the branchy code no test is watching.
scripts/crap.sh # report against the committed baseline
scripts/crap.sh --check # what CI runs
scripts/crap.sh --update # accept the current scores as the new baselineNeeds rustup component add llvm-tools-preview and
cargo install cargo-llvm-cov cargo-crap.
The gate is a ratchet against crap_baseline.json, not an absolute bar. The
cli::* command handlers carry real debt today — they are the thin layer
between argument parsing and the API, and the wiremock tests exercise the
logic underneath rather than the handlers themselves. Failing the build on
that now would only mean a permanently red build. What CI rejects is a change
that makes it worse: an existing function whose score rises, or a new function
that lands above the threshold of 30.
When a change legitimately alters the scores — you covered something, split a
function, or decided the debt is worth taking — run scripts/crap.sh --update
and commit crap_baseline.json alongside it. The baseline is sorted by
(file, function, line), so that diff stays readable.
MIT