Warning
This is an experimental ENS CLI preview. It is not production-ready, not released on npm, and may include frequent breaking changes.
Do not rely on it for production workflows, high-value ENS names, or critical ENS management.
A command-line tool for interacting with ENS (Ethereum Name Service). Built for autonomous AI agents but works for humans too.
The CLI handles two categories of operations:
- Read operations (resolution, availability, pricing) execute directly and return results.
- Write operations (registration, renewal, record setting) output unsigned calldata as JSON (
{to, data, value}) for the caller to sign and broadcast. For testing, you can send transactions with raw calldata at transact.swiss-knife.xyz/send-tx.
Built with Incur for agent-native features (MCP server mode, skills, token-efficient output) and viem for ENS resolution with full CCIP-read support.
npm install -g "https://pkg.pr.new/ensdomains/ens-cli/@ensdomains/cli@main"
ensThe @main tag tracks the latest main build. To update, rerun the installation with --prefer-online to check cache freshness:
npm install -g --prefer-online "https://pkg.pr.new/ensdomains/ens-cli/@ensdomains/cli@main"To pin a build, replace main in the URL with a commit hash (e.g. @ensdomains/cli@COMMIT_HASH).
For an optional no-install alternative, run:
npx "https://pkg.pr.new/ensdomains/ens-cli/@ensdomains/cli@main"# Forward resolve a name to an address
ens get address vitalik.eth
ens get address vitalik.eth --coin-type 0 # BTC record, as raw hex (scriptPubKey)
ens get address vitalik.eth --chain-id 10 # Optimism address (EVM addresses are checksummed)
# Reverse resolve an address to a name
ens get name 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
# Get a text record
ens get text vitalik.eth --key url
ens get text vitalik.eth --key com.twitter
# Get avatar URL
ens get avatar vitalik.eth
# Show owner, resolver, and expiry for any name, following ENSv2 resolution
ens whois vitalik.eth
ens whois sub.nick.eth
ens whois ens.domains# Check if a name is available
ens available myname.eth
# Check registration/renewal cost
ens price myname.eth
ens price myname.eth --duration 63072000 # 2 yearsRegistration is a two-step process. Both commands output calldata JSON.
# Step 1: Generate commit transaction
ens register commit myname.eth --owner 0xYourAddress --json
# Returns: { to, data, value, secret, ... }
# SAVE THE SECRET — you need it for step 2
# Step 2: Wait 60+ seconds after commit is mined, then generate register transaction
ens register reveal myname.eth \
--owner 0xYourAddress \
--secret 0x... \
--json
# Returns: { to, data, value }Options for both commands: --duration (seconds, default 1 year), --resolver, --subregistry, --paymentToken, and --referrer; use the same values for commit and reveal. On ENSv2, registration is paid in the ERC-20 token shown by ens price, so a nonzero --value is rejected, and both commands check the name, duration, and token with the registrar first. ENSv1 reveal requires --value with the freshly fetched bufferedTotal. --reverse-record is ENSv1-only; it does not deploy or select an ENSv2 resolver and does not create forward resolution. After ENSv2 registration, use ens set name <name> to set the owner's reverse record separately.
On ENSv2, --resolver defaults to the owner's deployed permissioned resolver when the canonical owned resolver already exists; otherwise it falls back to the zero address, registering a name with no resolver. Do not broadcast a zero-resolver commitment unless that is intentional. Generate ens resolver deploy first (see below), optionally initialize the name's address/text records in that deployment, and pass its predicted resolver address explicitly to both commit and reveal. The predicted address can be committed before its deployment transaction is broadcast, so the resolver can be deployed during the commitment wait.
Change the resolver for an already-registered name. Auto-routes between the v2 registry, the v1 NameWrapper (for wrapped names), and the v1 ENS registry (for unwrapped names).
ens resolver set myname.eth --resolver 0xResolverAddr --chain sepolia
# Returns: { to, data, value, version, wrapped, ... }Transaction must be sent from the name owner (or an approved operator).
Set the transaction sender's ETH reverse record.
ens set name myname.eth
# Returns: { to, data, value, name, reverseRegistrar }
# Clear the reverse record
ens set name ''The generated transaction must be sent from the address whose reverse record should be changed. A reverse record is only one half of a primary name. A functional primary name requires a bidirectional match: the name must forward-resolve to that same address, and the address's reverse record must point back to the name.
ENSv2 names use an OwnedResolver, a per-account PermissionedResolver proxy. The v1 Public Resolver can't be reused for updates because its authorisation is gated by the v1 registry, which knows nothing about v2-registered names. Each owner can deploy their own resolver through the v2 VerifiableFactory; ENSv2 registration, migration, and subname creation commands use that resolver by default when it is already deployed.
# Predicts the CREATE2 address and emits deployProxy calldata.
# alreadyDeployed=true short-circuits when the resolver already exists.
ens resolver deploy 0xYourAddress --chain sepolia --json
# Returns: { to, data, value, resolver, alreadyDeployed, ... }
# Deploy a new resolver and initialize records atomically.
ens resolver deploy 0xYourAddress \
--name myname.eth \
--records '[{"type":"address","address":"0xYourAddress","coinType":60},{"type":"text","key":"url","value":"https://example.com"}]' \
--chain sepolia \
--jsonThe resolver address is derived from (factory, proxyLogic, deployer, salt), so the deploy transaction must be sent from deployer. The salt defaults to keccak256(abi.encode(keccak256("OwnedResolver"), owner, 0)) where owner is the admin the resolver is initialized with — the canonical scheme shared with the contracts-v2 setup script and the manager app's migration flow, so the resolver can be rediscovered from the owner address alone.
--name and --records must be used together. On a new resolver, the address, text, and contenthash operations use PermissionedResolver.initialize(grants, calls) to initialize records in the deployment transaction, avoiding a later ens set batch transaction. If the resolver is already deployed, use ens set batch instead. The predicted resolver address can be used in a registration commitment before deployment, so deployment and record initialization can happen during the commitment waiting period.
Sepolia targets the October 1, 2026 contracts-v2 deployment. Mainnet targets the October 26, 2026 deployment, which the mainnet Universal Resolver now reports as active. On both chains the CLI only uses the ENSv2 addresses once the Universal Resolver reports ENSv2. PermissionedResolver setters take DNS-encoded names (setAddress, setText, setContenthash); PublicResolverV2 retains the v1 namehash-based setters despite using v2 ownership. The set commands detect the target resolver's interface, including when --resolver is supplied. When a .eth 2LD is registered in ENSv2 but the target resolver has no ENSv2 setters and no ROOT_REGISTRY() (which tells it apart from PublicResolverV2), the output includes a warning: if the resolver authorizes through ENSv1 registry ownership, as the ENSv1 Public Resolver does, the transaction will revert, because migration gives that ownership up. Subregistry and resolver initializers grant roles through arrays of (account, roleBitmap) entries.
The CLI discovers the .eth registry through the selected Universal Resolver's ROOT_REGISTRY() and getSubregistry("eth"), and checks it against the configured registrar's ETH_REGISTRY(). Resolver address prediction reads proxyLogic() from the factory. Registrar, renewer, factory, migration controller, implementation, and default payment token addresses remain deployment configuration.
Other options: --admin (defaults to deployer), --salt (decimal or 0x hex), --role-bitmap (decimal or 0x hex, default 0x1111…1111).
ENSv2 subnames are created inside a parent name's subregistry. A name must have a subregistry set before ens subname create can register children under it.
# Deploy a UserRegistry for a name if it does not already have one.
ens subregistry deploy parent.eth --deployer 0xYourAddress --chain sepolia --json
# Wire the deployed registry into the parent name.
ens subregistry set parent.eth --registry 0xSubregistry --chain sepoliasubregistry deploy returns alreadySet=true when the name already has a subregistry. The proxy address is derived from (factory, deployer, salt), so the deploy transaction must be sent from --deployer. The default salt is keccak256(abi.encode(keccak256("UserRegistry"), namehash(name), 0)), and the default initializer grants the root account all UserRegistry roles.
# Check cost first
ens price myname.eth --renew
# Generate renewal calldata
ens renew myname.eth --value 2307947853431408 --json
# ENSv2 uses ERC-20 payments. Approve the returned registrar to spend total.
ens price myname.eth --renew --chain sepolia
ens renew myname.eth --chain sepolia --jsonOn an active ENSv2 deployment, renewal targets ETHRegistrar for v2 names and ETHRenewerV1 for reserved, unmigrated v1 names, including expired reservations in grace. Both use renew((label, duration, referrer), paymentToken). Use the same --payment-token and --duration for pricing and renewal. ENSv2 renewal sends zero ETH; a nonzero --value is rejected. renew and price --renew return an error when the contract would refuse the renewal, such as for an unregistered name or an unsupported token. On ENSv1, --value remains required and should be the freshly fetched bufferedTotal. Renewal quotes exclude the registration premium.
Generate the one-way migration transaction for a pre-migrated (reserved) .eth 2LD. The command reads the v1 owner, resolver, wrapping state, and fuses, then targets the unlocked or locked migration controller as appropriate.
ens migrate myname.eth --chain sepolia --json
# Returns: { to, data, value, kind, currentOwner, owner, controller, ... }
# Optionally change v2 configuration during migration
ens migrate myname.eth \
--chain sepolia \
--owner 0xNewOwner \
--resolver 0xResolver \
--subregistry 0xSubregistryThe transaction must be sent by the current ENSv1 token owner or an approved operator. Unwrapped names transfer the Base Registrar ERC-721 to the unlocked controller. Wrapped names transfer the NameWrapper ERC-1155 to either the unlocked or locked controller based on CANNOT_UNWRAP. Locked migration deploys its own WrapperRegistry, so --subregistry is ignored in that case.
Unless --resolver is provided, migration uses the new owner's canonical OwnedResolver when it is already deployed. Otherwise it preserves the current ENSv1 resolver for record continuity and returns a flag recommending that an OwnedResolver be deployed before regenerating the migration. When that resolver lacks ENSv2 record setters, the flag is a warning: if it authorizes updates through ENSv1 registry ownership, as the ENSv1 Public Resolver does, its records keep resolving after migration but can no longer be changed until ens resolver set points the name at a new resolver. Deploy the OwnedResolver with --name and --records to carry records over. Locked names with CANNOT_SET_RESOLVER are excluded because their resolver payload is ignored.
Wrapped-name flags also describe how the command inferred the migration path, the sender authorization it assumes, and any fuse-dependent behavior to review before broadcasting. For example, CANNOT_APPROVE with a frozen token approval is expected to revert.
Migration only works after the name has been reserved in ENSv2 and cannot be reversed. Names in the ENSv1 grace period must be renewed through ETHRenewerV1 before migration. Test on Sepolia before migrating valuable names.
Generate calldata to create a subname under a parent you own. On ENSv2, the command walks the registry hierarchy and targets the parent's subregistry. If the parent has no subregistry, deploy one with ens subregistry deploy and set it with ens subregistry set first. On ENSv1, the command reads the parent's onchain owner; if the parent is wrapped in the NameWrapper, the calldata targets the NameWrapper instead of the registry.
# Create an ENSv2 subname under a parent with a subregistry
ens subname create sub.parent.eth --owner 0xNewOwner --chain sepolia
# Create a subname under an unwrapped parent (registry.setSubnodeRecord)
ens subname create sub.parent.eth --owner 0xNewOwner
# Create a subname under a wrapped parent (NameWrapper.setSubnodeRecord)
ens subname create sub.wrapped.eth --owner 0xNewOwner --fuses 0 --expiry 0
# Custom resolver
ens subname create sub.parent.eth --owner 0xNewOwner --resolver 0x...Options: --resolver (defaults to the owner's deployed resolver on ENSv2 when found, chain public resolver on ENSv1), --subregistry, --duration, and --role-bitmap (ENSv2), --fuses and --expiry (NameWrapper only, both default to 0).
All set commands output calldata JSON. The target resolver is read from the Universal Resolver by default — the name must already have a resolver set, otherwise the command errors. Pass --resolver <address> to override.
# Set address record
ens set address myname.eth --address 0x1234...
ens set address myname.eth --address 0x1234... --coin-type 0 # BTC
# Set text record
ens set text myname.eth --key url --value https://example.com
ens set text myname.eth --key com.twitter --value @myhandle
# Set content hash
ens set contenthash myname.eth --hash 0x...
# Batch set multiple records (multicall)
ens set batch myname.eth --data '[
{"type": "text", "key": "url", "value": "https://example.com"},
{"type": "text", "key": "com.twitter", "value": "@myhandle"},
{"type": "address", "address": "0x1234...", "coinType": 60}
]'# Send feedback about the CLI to the maintainers
ens feedback "It would be great to have a command for X"
# Optionally include your name and a way to reach you
ens feedback "Found a bug in ens set" --name Alice --contact alice@example.comRPC endpoint (in order of precedence):
--rpc <url>flagETH_RPC_URLenvironment variable- Public fallback RPCs
A custom RPC from --rpc or ETH_RPC_URL is used on its own; the CLI never falls back to public RPCs when one is set.
Chain selection:
ens get address vitalik.eth --chain mainnet # default
ens get address vitalik.eth --chain sepoliaOutput format:
ens get address vitalik.eth --json
ens get address vitalik.eth --format yamlThe CLI has built-in support for AI agent discovery:
# View the LLM-readable command manifest
ens --llms
# Register as an MCP server
ens mcp add
# Generate skill files for agents
ens skills add
# Run in MCP stdio mode
ens --mcp- Bun v1.0+
git clone https://github.com/ensdomains/ens-cli.git
cd ens-cli
bun install# Run any command directly
bun src/index.ts get address vitalik.eth
# With a custom RPC
bun src/index.ts get address vitalik.eth --rpc http://localhost:8545
# With environment variable
ETH_RPC_URL=http://localhost:8545 bun src/index.ts available myname.ethbun run test runs the CLI against local anvil forks of Sepolia and mainnet. Each test generates calldata with the real CLI, sends it from an impersonated account, and checks the resulting onchain state. Foundry must be installed. The forks use public RPCs by default; set SEPOLIA_RPC_URL or MAINNET_RPC_URL to use your own.
e2e/sepolia.test.tscovers ENSv2 registration, records, primary names, renewals, subregistries, and subnames, plus ENSv1 names reserved in ENSv2: renewal throughETHRenewerV1, v1 resolver and subname routing, and all three migration paths.e2e/mainnet.test.tscovers the live mainnet ENSv2 deployment: registration paid in USDC, records, primary names, renewals, subregistries, and subnames, plus renewal of reserved names throughETHRenewerV1, unwrapped and wrapped locked migrations, and errors for transactions that would revert.
- One positional argument per command: the subject it operates on (an ENS name or an address). Everything else is a named option, whether required or optional. Example:
ens set text myname.eth --key url --value https://example.com— neverens set text myname.eth url https://example.com. Multiple positionals force the caller to memorize parameter order; named options are self-documenting in scripts, shell history, and agent transcripts. - Required options are simply non-
.optional()in the Zod options schema — validation fails with a clear error if they're missing. - No single-character option aliases. Spell out
--resolver, not-r. - Group related commands (
get,set,register,resolver,subname) rather than adding top-level commands for each operation.
- Create a command file in
src/commands/exporting a function that returns a command definition (for flat commands) or aCli.create()group (for nested subcommands). - Follow the command design conventions above.
- Mount it in
src/cli.tsvia.command(). - Add end-to-end coverage in
e2e/and runbun run test.