merod CLI Reference
merod is the node daemon. Every invocation takes two global arguments, then a
subcommand.
Global arguments
Section titled “Global arguments”| Flag | Type | Default | Notes |
|---|---|---|---|
--home <PATH> |
path | platform node directory | Directory for config and data. Env: CALIMERO_HOME. |
-n, --node <NAME> |
name | — (required) | Name of the node; its home is <home>/<NAME>. |
merod --home data/ --node node1 <subcommand>Subcommands: init, config, run (alias up), kms, account.
Initialize node configuration: creates the home directory, generates an
ed25519 identity keypair, writes config.toml, opens the datastore, and
provisions the node’s account root (see account).
| Flag | Type / values | Default | Purpose |
|---|---|---|---|
--boot-nodes <ADDR> |
multiaddr (repeatable) | — | Explicit bootstrap node multiaddrs. |
--no-account-root |
bool | false |
Start with no account root: the root stays in cold storage and this node’s device is enabled by a certificate signed elsewhere. Such a node cannot certify a device or be the holder half of a pairing. |
--boot-network <NETWORK> |
calimero-dev, ipfs |
calimero-dev |
Use a known network’s bootstrap nodes. |
--swarm-host <HOST> |
IP list | 0.0.0.0,:: |
Hosts to listen on for the libp2p swarm. |
--swarm-port <PORT> |
u16 | 2428 |
Swarm (peer-to-peer) port. |
--server-host <HOST> |
IP list | 127.0.0.1,::1 |
Hosts to listen on for the RPC server. |
--server-port <PORT> |
u16 | 2528 |
RPC / admin / JSON-RPC / WS / SSE port. |
--auth-mode <MODE> |
proxy, embedded |
proxy |
Authentication mode for server endpoints. |
--auth-storage <KIND> |
persistent, memory |
persistent |
Embedded-auth storage backend (only with --auth-mode embedded). |
--auth-storage-path <PATH> |
path | auth |
Embedded-auth storage path (persistent storage only). |
--mdns |
bool | false |
Enable mDNS local discovery (LAN / offline dev; off by default). |
--advertise-address |
bool | false |
Advertise the node’s observed address. |
--external-address <MULTIADDR> |
multiaddr (repeatable) | — | Static external multiaddrs to advertise. Pair with --advertise-address for them to be advertised. |
--rendezvous-registrations-limit <N> |
usize | 3 |
Max rendezvous registrations. |
--relay-registrations-limit <N> |
usize | 3 |
Max relay registrations. |
--autonat-probe-interval <SECS> |
u64 | 10 |
Interval between AutoNAT probes, in seconds. |
--autonat-max-candidates <N> |
usize | 5 |
Max untested address candidates to probe with AutoNAT. |
--force |
bool | false |
Wipe and re-initialize even if the directory already exists. |
--mode <MODE> |
standard, read-only |
standard |
Node operation mode (read-only disables JSON-RPC execution). |
# Initialize with the default network and portsmerod --node node1 init
# Initialize with custom portsmerod --node node1 init --server-port 2528 --swarm-port 2428config
Section titled “config”Update keys in the node’s config.toml. Arguments are key=value pairs where
the key is a dotted TOML path. The change is validated by reloading the
resulting config before it is written.
# Quote the argument so your shell does not glob the bracketsmerod --node node1 config "server.listen=['/ip4/127.0.0.2/tcp/3000', '/ip6/::1/tcp/3000']"There are no flags — only positional key=value arguments (repeatable).
Run the node: loads and validates config.toml, then starts the swarm and RPC
server. Foreground process. Alias: up.
| Flag | Type / values | Default | Purpose |
|---|---|---|---|
--auth-mode <MODE> |
proxy, embedded |
— | Override the authentication mode from config.toml for this run. |
--mock-tee |
bool | false |
DEV/TEST ONLY. Produce/accept mock TEE attestation quotes. Env: MEROD_MOCK_TEE. |
merod --node node1 runmerod --node node1 up --auth-mode embeddedKMS (key management service) operations for TEE-enabled nodes.
kms probe
Section titled “kms probe”Probe the KMS attestation and storage-key fetch flow against the node’s configured TEE/KMS settings.
| Flag | Type | Default | Purpose |
|---|---|---|---|
--kms-url <URL> |
URL | — | Override the configured KMS URL for this probe. |
--json |
bool | false |
Emit the probe result as machine-readable JSON. |
merod --node node1 kms probemerod --node node1 kms probe --jsonaccount
Section titled “account”The account-root command family. These are the operations whose whole point is that the signing key does not have to reach a running node — so most of them can run on a machine with no node at all.
merod --node node1 account <export|import|device|sign-cert|revoke-proof|warrant>Where an account root comes from
Section titled “Where an account root comes from”Exactly two places, and nothing else mints one:
| How | Result |
|---|---|
merod init |
Provisions a root. Every ordinary node. |
merod account import |
Restores one from a 24-word phrase. |
merod init --no-account-root |
No root, deliberately. The root stays in cold storage; this node’s device is enabled by a certificate signed elsewhere. |
Nothing provisions a root lazily. Any path that needs one and doesn’t find one fails with an error naming these three options, rather than generating a key that certifies nothing anyone recognises.
A root-free node can still name its account — a certified device row answers before the root fallback. What it cannot do is certify a device (its own or anyone’s), so it cannot self-enrol or be the holder half of a pairing.
sign-cert
Section titled “sign-cert”Certify a device for an account, offline. This closes a specific gap: pair-complete
mints certificates but needs a running node holding the root, so a thin client — a
phone, a script, anything that runs no application and joins no group — had no way to
obtain the credential it needs to present itself.
There are two independent choices, which is why the flag list looks larger than the number of real modes.
Choice 1 — who mints the device keys
Section titled “Choice 1 — who mints the device keys”| Mode | Flags | Who ever holds the device secret |
|---|---|---|
| Certify | --device --sign-pk --kem-pk (all three) |
The client. It minted its own keypairs and sent you only public halves. |
| Generate | --generate (conflicts with the three above) |
This machine. It mints the device and prints the secret to stdout. |
Prefer Certify. --generate exists for clients that cannot mint their own — a
provisioning script, a fresh install — and it prints a signing key, so the holder is
trusting this machine with it. A client that can mint its own device should, because
then the secret never exists anywhere but there.
Choice 2 — where the root comes from
Section titled “Choice 2 — where the root comes from”| Mode | Flag | Requires |
|---|---|---|
| Node’s store | (default) | A stopped node that holds the root. |
| Recovery phrase | --from PATH |
Nothing. Opens no datastore, so it runs on a machine with no node — no home, no store, no init. |
--from is what makes an air-gapped signing machine possible, and it composes with
either choice above.
Commands
Section titled “Commands”# Certify a client's own device, on a machine with no node (the recommended shape).merod account sign-cert \ --device <64-hex DeviceId> \ --sign-pk <64-hex ed25519 public key> \ --kem-pk <64-hex x25519 public key> \ --from ./phrase.txt
# Mint and certify in one step, for a client that cannot mint its own.merod account sign-cert --generate --from ./phrase.txt
# Read the root from a stopped node's store instead of a phrase.merod --node node1 account sign-cert --device <HEX> --sign-pk <HEX> --kem-pk <HEX>
# Re-issue after a device re-key. The epoch must strictly advance.merod account sign-cert --device <HEX> --sign-pk <HEX> --kem-pk <HEX> \ --device-epoch 1 --from ./phrase.txtOutput is the hex credential on the first line, then Account:, Device:, and —
under --generate only — Secret:, followed by a hint showing the meroctl context intent invocation that consumes it.
Three limits, all deliberate
Section titled “Three limits, all deliberate”-
It cannot check the device id — for two separate reasons. The signer does know the account: it derives it from the root it holds, and prints it on the
Account:line, so an operator can at least confirm the account is the intended one. What it cannot confirm is the--devicevalue.DeviceIdisH(account ‖ nonce)where the nonce is 16 random bytes chosen by whoever minted the device, and the client sends onlydeviceId,signPkandkemPk— never the nonce. So the signer cannot recompute the id to compare it. Separately, the id deliberately excludes the keys so that a device survives a re-key, which means the id cannot be checked against--sign-pk/--kem-pkeither.Consequence: nothing here can tell a mistyped id from a real one. A certificate naming a device nobody holds is inert rather than dangerous — there is no matching secret, so it authorises no one. The failure mode is a wasted certificate, not a forged one.
-
Epoch 0 only. The certificate is signed at key epoch 0 with an empty handoff chain. An account whose root has rotated needs the chain up to the signing epoch, and nothing in this CLI produces one.
-
--device-epochmust strictly advance. The projection refuses a link that does not advance it, so re-issuing at the same epoch is inert rather than a rollback.
Why any of this works offline: the certificate is self-certifying — it carries the genesis and the root-key chain, so a verifier checks it from the account id alone with no folded state. That is why it does not matter who publishes or presents it.
revoke-proof
Section titled “revoke-proof”Sign a device revocation offline, to be published by any node. Same shape and same
reasoning as sign-cert, and also epoch 0 only. --from means it needs no store at
all.
merod account revoke-proof --device <64-hex DeviceId> --from ./phrase.txtdevice
Section titled “device”Print this node’s device id and public keys.
--reset drops the stored device row so the next pairing or namespace join mints
a fresh one.
It drops a row that is revoked in any namespace this node takes part in, or that
no namespace ever certified: neither can ever serve again, because revocation is
terminal and an uncertified id authorises nobody.
It refuses a row that is certified somewhere and that no revocation has reached,
because as far as this store can tell that device is live, and its id is the
replica lineage the node’s CRDT state is held under - dropping it would strand
that state with nothing able to author there again.
An online node normally never needs the command. When its device is revoked the group key rotates, the node strands on the new epoch, and the key-recovery pull releases the spent row and mints a fresh device on its own; pair-init releases a revoked or uncertified row the same way.
--force overrides the one refusal, and exists for a device revoked while its
node was offline: the tombstone never reached this store, so the guard sees a
live device and the operator has to say otherwise.
Like export and import it opens the datastore directly, so the node must be
stopped.
merod --node node1 account device --resetmerod --node node1 account device --reset --forcewarrant
Section titled “warrant”Sign a warrant authorising one relay to perform one intent, once. Offline by construction: it opens no store and contacts no node, because a warrant is a statement about an intent rather than about any node’s state. The device secret signs it locally and is never sent — only the signature travels.
merod account warrant \ --context <hex ContextId> \ --method set \ --args '{"key":"k","value":"v"}' \ --executor <64-hex AccountId of the relay> \ --nonce 1 \ --device-secret <64-hex> \ --credential <hex from sign-cert>export and import
Section titled “export and import”# Print the 24-word phrase to stdout.merod --node node1 account export
# Write it to a file instead. Refused without the second flag; created 0600.merod --node node1 account export --out backup.txt --allow-plaintext-file
# Restore. Reads stdin by default, or --from PATH.merod --node node1 account importA backup is one secret. The account id is H(genesis(root_pk)) — a pure function
of the root key with no per-namespace nonce — so it is the same account in every
namespace, and the phrase alone recovers all of it.
Import refuses to replace a root that has already certified a device, because that
is unrecoverable and there is no second copy. Restoring onto a freshly-inited node
whose own root has certified nothing needs no --force.
Example: a long-lived node
Section titled “Example: a long-lived node”A hosted node that should rejoin the network across restarts is initialized once
with a persistent home, has its config.toml adjusted for the deployment, then
runs as a foreground service (under your process supervisor).
-
Initialize under a stable home directory. Keep the RPC server on loopback so it is only reachable through a local reverse proxy:
Terminal window merod --home /var/lib/calimero --node node1 init \--server-port 2528 --swarm-port 2428 -
Edit
config.tomlfor a datacenter deployment — turn off LAN discovery, dial known peers on startup, and advertise a stable public swarm address instead of relying on AutoNAT. These keys live in the fileinitwrote (<home>/node1/config.toml):[bootstrap]nodes = ["/ip4/198.51.100.7/tcp/2428/p2p/12D3KooW…<peer-id>",][discovery]mdns = falseadvertise_address = trueexternal_address = ["/ip4/203.0.113.10/tcp/2428"]You can apply individual keys without hand-editing using the
configsubcommand, which validates the result before writing:Terminal window merod --home /var/lib/calimero --node node1 config "discovery.mdns=false" -
Run the node. It reloads and validates
config.toml, then stays in the foreground:Terminal window merod --home /var/lib/calimero --node node1 run