Skip to main content

CLI Reference

The Poweur ID CLI (poweur) puts your ID in the terminal: identity and keys, messages, contacts and relay diagnostics. Use it on your desktop to message people without switching apps; give it to an AI coding agent (Claude Code, Codex, Cursor…), which can drive every command and read --json output; or run it in CI, cron jobs and bots. See Clients overview for examples.

Installation​

The CLI is a single Go binary. Build it from the repository (Go 1.25 or newer):

git clone https://github.com/romanmandryk/poweur.git && cd poweur/apps/cli
go build -o poweur .
sudo mv poweur /usr/local/bin/ # or anywhere on your PATH
poweur --help

To try it without installing, run go run . --help in apps/cli. The TypeScript SDK ships a poweur CLI with the same commands.

Configuration​

Configuration is stored at ~/.poweur/config.toml. The CLI stores a default identity plus a root keys directory; you can override the identity per command.

# ~/.poweur/config.toml
relay_url = "https://relay.poweur.net"
identity = "mybot.poweur.net"
keys_dir = "~/.poweur/keys"

Individual settings can be overridden via environment variables:

SettingEnv var
Relay URLRELAY_URL
Identity subdomainIDENTITY
Keys directoryKEYS_DIR
Key-file passphrasePOWEUR_KEY_PASSPHRASE

Global Flags​

All commands accept these global flags:

FlagDescription
--jsonOutput machine-readable JSON instead of human-friendly text
--use-identity <subdomain>Override the active identity for this command

Commands​

Run poweur for the list of commands with a line each, poweur <command> for that command's subcommands with their <required> and [optional] arguments, and poweur <command> <subcommand> --help for one. A command that needs a subcommand exits non-zero and prints that list.

poweur key enroll also registers the new machine with the relay, so it appears in Keys & devices (named for its label or hostname, with client cli and its platform) as soon as it is approved.

poweur identity create <name>​

Generate a new long-lived Ed25519 signing keypair and an X25519 encryption keypair, then register with the configured relay. Two modes:

Hosted (--hosted): no DNS token. The client submits a signed identity document; the relay persists it under HOSTED_DOMAINS (e.g. *.poweur.net wildcard). Keys are published via /.well-known/poweur/ (see Web identity).

poweur identity create alice --hosted --relay http://127.0.0.1:8080

Self-hosted (DNS token): the relay writes DNS records using your provider token:

  • _poweur.<identity> — identity public key
  • _poweur-enc.<identity> — encryption public key
  • <identity> — A/CNAME routing record
poweur identity create alice --dns-provider cloudflare

Every identity is seed-based: both long-lived keys are derived from a single 32-byte master seed, so the whole identity can be recovered from those 32 bytes alone. See Key management & recovery.

poweur identity create alice --hosted --relay http://127.0.0.1:8080

The CLI generates the seed and prints it (with its 24-word mnemonic) — this is the only copy. --seed <base64url> uses a seed you already hold instead, and never re-prints it.

Private keys are written to ~/.poweur/keys/ (<identity>.key and <identity>.enc). Existing files are never overwritten: a second create for a name this machine already holds fails and points you at key enroll / key recover. If the relay refuses the registration (name taken, policy, unreachable), the CLI deletes the key files it just wrote so they cannot shadow a later enroll or recover.

Flags:

FlagDescription
--hostedHosted registration (no DNS token; requires HOSTED_DOMAINS on relay)
--invite-code <code>Invite code when the relay gate is invite
--dns-provider <name>DNS provider (cloudflare | hetzner) — self-hosted mode
--dns-token <value>DNS API token (prefer CLOUDFLARE_API_TOKEN / HETZNER_API_TOKEN)
--parent-domain <domain>Parent domain used when a handle is provided
--relay <url>Relay URL override
--seed <base64url>Derive both keys from an existing master seed instead of generating one (never re-printed)
--use-identity <subdomain>Override identity for this command
--jsonMachine-readable output

Example (JSON output):

poweur identity create alice --dns-provider cloudflare --json
{
"identity": "alice.poweur.net",
"public_key": "MCowBQYDK2VwAyEAn3a7...",
"encryption_public_key": "2qsV0x9Ru9v3o_VzH7mHsH-yjwI5sOqO6sRpCVoqXxA",
"key_path": "~/.poweur/keys/alice.poweur.net.key",
"encryption_key_path": "~/.poweur/keys/alice.poweur.net.enc",
"relay": "https://relay.poweur.net",
"registered": true,
"seed": "…",
"mnemonic": "…"
}

Unless you passed --seed, the response carries the generated "seed" (unpadded base64url) and its "mnemonic". They are returned exactly once — store them before the process exits:

poweur identity create alice --hosted --json | jq -r .seed > alice.seed
chmod 600 alice.seed

poweur identity show​

Display the current identity's subdomain, identity public key, and (if available) encryption public key.

poweur identity show

poweur identity dns <identity>​

Perform a live DNS lookup to verify that the identity's TXT (public key), TXT (encryption key), and A/CNAME records are propagated.

poweur identity dns alice.poweur.net

poweur identity lookup <identity>​

Resolve an identity using the web-first chain (HTTPS /.well-known/poweur/id.json, then DNS TXT). Prints source (web / dns / both), keys, the safety number, and the relay. Prefer this over identity dns for hosted identities that have no per-user TXT records.

It also reads the identity's public self-description from the same well-known route — profile.json (display name, bio, locale, avatar, links) and capabilities.json (features and endpoint hints), both served world-readable out of .poweur/public/. Both are optional: an identity that publishes neither still looks up fine, and only an unreachable host (as opposed to a 404) prints a note on stderr. When there is no capabilities.json, the identity document's own capabilities list is shown instead — the same fallback the web client uses, so the two surfaces degrade to the same answer.

poweur identity lookup alice.poweur.net
poweur identity lookup alice.poweur.net --json

In --json, capabilities stays the identity document's string list and capabilities_document carries capabilities.json when one is published; profile is the profile document or null.


poweur identity use <identity>​

Update the default identity stored in ~/.poweur/config.toml.

poweur identity use id2.poweur.net

poweur identity list​

List all identities discovered in the keys directory. The current default is marked with *.

poweur identity list

poweur send <to> <message>​

Sign and send an end-to-end-encrypted message. The CLI:

  1. Loads (or creates) a session for the active identity via ensure session.
  2. Generates a client-side message id (ULID-shaped) and writes a queued entry to the local pending journal (~/.poweur/pending/<identity>.jsonl).
  3. Looks up the recipient's X25519 encryption public key at _poweur-enc.<recipient>. If no record is found, the send is aborted. There is no plaintext fallback.
  4. Encrypts the payload with ChaCha20-Poly1305 under an X25519-derived key (see End-to-End Encryption).
  5. Signs the canonical envelope (including the id: line and the enc: line) with the session private key.
  6. Attaches session_id and session_proof so the recipient's relay can verify without contacting the sender's relay.
  7. Resolves the recipient's home relay via DNS and POSTs /messages directly there (default direct-send model). On 202 Accepted the journal advances to delivered_recipient_relay (tick 1).
poweur send bob.example.org "Hey Bob, are you there?"

Flags:

FlagDescription
--use-identity <identity>Send from a specific identity (overrides active identity)
--via-home-relayPrivacy proxy: POST to the configured home relay (relay_url) instead of directly to the recipient's relay. The home relay accepts because the sender is locally hosted, then forwards to the recipient relay. Hides the sender's IP from the recipient relay at the cost of an extra hop.
--sign-with <session|identity>Choose the signing key (default session). Identity-signed sends omit session_id/session_proof.
--type <type>Envelope message type. Omit for ordinary chat: absent means chat.text, and the absent form is what keeps the signed canonical string identical to a pre-typing client's. sys.* is reserved — an unregistered one is refused locally.
--thread <id>Group this message into a conversation thread. Opaque to the relay.
--expires <rfc3339>When the message stops being meaningful. Expired envelopes are refused with HTTP 410.
--attach <file>Upload (max 20 MB), grant read to the recipient, and send a chat.attachment reference. The caption is optional.
--meta <key=value>Envelope metadata, repeatable. Plaintext — addressing, not content. A duplicate key is an error rather than a silent overwrite.
--jsonMachine-readable output

--type, --thread, --expires and --meta are validated before the message is encrypted, signed or journalled, so a typo never becomes a recorded send attempt. They cannot be combined with --anon: an unsigned envelope binds nothing, so the fields would be routing metadata nobody could trust. See Typed Messages.

poweur send bob.example.org "the logo, v3" --thread thr_rebrand --attach ./logo.png

If the relay reports the session expired, the CLI silently re-registers a session and retries once. Network failures, 429s and 5xx responses queue an identity-signed encrypted envelope with capped exponential backoff; poweur listen retries on reconnect and poweur outbox list|retry exposes it explicitly. Permanent 4xx responses still write a sticky failed entry.

Note that cfg.RelayURL (the configured relay_url) is the home relay — it is used for inbox polling, ack delivery, identity admin, and (only when --via-home-relay is set) outbound sends.


poweur inbox​

Fetch and display messages from the relay inbox for the active identity. The CLI:

  1. Ensures a valid session.
  2. Fetches a challenge from the relay, signs it with the session key, and calls GET /messages/:identity.
  3. Decrypts any envelope with encryption metadata using the local X25519 private key. Decrypted messages are prefixed with 🔒 in human output.
  4. For every successfully decrypted message, signs and POSTs a delivered_client ack to the original sender's home relay (DNS-resolved). The local pending journal records this as the source of tick 2 for that conversation partner.
  5. Drains the response's acks array, advancing the local pending journal to delivered_client for any referenced message ids — this is how the sender learns about tick 2.
  6. Silently re-registers and retries if the relay reports the session expired.
poweur inbox

Flags:

FlagDescription
--use-identity <identity>Fetch inbox for a specific identity
--jsonRaw JSON output (including undecrypted envelope)
--decryptWith --json: do the full pickup above and add decrypted and body (the plaintext) to each message. A script or bot gets readable text without holding the identity's message key. A message that could not be opened keeps its ciphertext in payload, with the failure text as body and decrypted: false. Anything meant for a person goes to stderr, so stdout is one JSON document. Refused without --json. poweur listen takes the same flag (plus --once)

poweur messages status​

Show the local pending journal for the active identity. Each line of ~/.poweur/pending/<identity>.jsonl is collapsed to the latest state per message_id, then rendered with WhatsApp-style tick glyphs:

GlyphStateMeaning
·queuedSend pipeline started, no relay response yet
✓delivered_recipient_relayRecipient relay returned 202 Accepted (tick 1)
✓✓delivered_clientRecipient client acked successful decrypt (tick 2)
✗failedSend pipeline gave up; sticky
poweur messages status
poweur messages status --id msg_01j9xkay7g000000000000000
poweur messages status --json

Flags:

FlagDescription
--use-identity <identity>Read the journal for a specific identity
--id <message_id>Show only the entry for the given message id
--jsonMachine-readable JSON output

See Delivery Acks for the full state model.


poweur session status​

Show the locally cached session for the active identity.

poweur session status

poweur session refresh​

Force-delete the cached session and register a new one. Useful when debugging or rotating keys without waiting for the 24h TTL.

poweur session refresh

poweur session revoke​

Delete the local session file only (does not call DELETE /sessions/:id on the relay). The next send or inbox call will transparently re-register.

poweur session revoke

poweur relay status​

Check relay connectivity. Displays the configured relay endpoint and relay version, and reports whether the relay is reachable.

poweur relay status

Output:

Relay:    https://relay.poweur.net
Status: ✓ OK
Version: 0.1.0
Latency: 42ms

JSON output (--json):

{
"relay": "https://relay.poweur.net",
"status": "ok",
"version": "0.1.0",
"latency_ms": 42
}

poweur key rotate​

Move the identity onto a new master seed: both the signing and the encryption key change, and the new seed and mnemonic are printed — the only copy. A rotation statement is published so verifiers accept the new signing key while the old one stays valid for a grace period (see Web identity); contacts who pinned the old key follow the statement rather than warning. The old key files are kept beside the new ones as *.pre-rotate (the old encryption key still opens mail sent to it).

Rotation is how you lock out a device that may be compromised: every device holds the identity key, so removing a device does not stop it, but a new key does. Your other devices must then be paired again.

poweur key rotate --grace 168h
FlagDescription
--grace <duration>How long the previous key stays valid (default 168h)
--use-identity <subdomain>Identity to rotate
--jsonMachine-readable output

poweur key derive --seed <base64url>​

Print the public keys a master seed derives — offline, writing nothing. Use it to check a recovery seed against a published identity document before trusting it.

poweur key derive --seed "$(cat alice.seed)" --json
{
"public_key": "gq2n...",
"encryption_public_key": "CJ5t..."
}

Compare against what the relay serves:

curl -s https://relay.poweur.net/identities/alice.poweur.net | jq .public_key
FlagDescription
--seed <base64url>Master seed (required)
--jsonMachine-readable output

poweur key recover <identity> --seed <base64url>​

Rebuild an identity's private key files from its master seed. Offline — no relay call, no network. The relay already holds the public half, so restoring the private half locally is all that is needed to use the identity again.

poweur key recover alice.poweur.net --seed "$(cat alice.seed)" \
--relay https://relay.poweur.net
poweur inbox

Writes ~/.poweur/keys/<identity>.key and .enc, and sets the identity, keys directory and relay in ~/.poweur/config.toml — so it works on a machine with no prior Poweur config.

FlagDescription
--seed <base64url>Master seed (required)
--relay <url>Relay to configure (needed on a fresh machine)
--parent-domain <domain>Parent domain to configure
--jsonMachine-readable output
caution

Recovery is pure local key derivation, so it always "succeeds" — a wrong seed produces valid keys that simply are not this identity's. Verify with poweur key derive first, or the failure surfaces later as a rejected request from the relay.


poweur key kit --seed <seed-or-mnemonic>​

Render a master seed as a recovery kit — 24 BIP39 words plus the base64url seed. Offline: it converts, it does not generate or store.

poweur key kit --seed "$(cat alice.seed)"

The mnemonic is an encoding of the same 32 bytes, not a second secret. It earns its keep only where a human copies the seed by hand: the checksum catches transcription slips, and the wordlist avoids the l/I/1 and O/0 confusions of base64url. The carrier is up to you — a printed card, a text file, a password-manager entry are all equally valid.

Every --seed flag in the CLI accepts either encoding, so a pasted kit just works.


poweur key ls​

The "Keys & devices" inventory: every enrollment registered for an identity, with kind, role, label and last-used time. Metadata only — listing devices needs no access to the wrapped seeds.

poweur key ls --json

An identity with no enrollments says so explicitly, and reminds you the seed is then the only way back.


poweur key enroll <identity> / key approve / key claim​

Pair a new device with one that holds the identity. The relay only relays: it cannot read the seed, and cannot pair a device of its own.

On the new device:

poweur key enroll alice.poweur.net --relay https://relay.poweur.net --label "work laptop" --wait

It prints a QR code for the Poweur app (poweur://pair?…), the browser link, the command below, and an 8-character code like K7QM-4XP2. On a device that already holds the identity (approving needs no recovery kit: the CLI keeps the identity's seed next to its keys, as ~/.poweur/keys/<identity>.seed), either use the link — nothing to compare, the link carries the new device's commitment:

poweur key approve 'https://alice.poweur.net/app/#pair=K7QM4XP2.…&id=alice.poweur.net'

— or type the code; both machines then show six digits, and this side delivers only once they are confirmed (typed at the prompt, or given with --sas):

poweur key approve K7QM-4XP2

Without --wait, run poweur key claim alice.poweur.net K7QM4XP2 on the new device to move each step along (the pairing is kept in ~/.poweur/pairing); scripts use key approve --no-wait then --sas the same way.

FlagCommandDescription
--label <text>enrollDevice description shown to the approver
--waitenrollKeep going until approved, then install the keys
--seed <value>approveMaster seed (base64url or mnemonic). Default: the seed this device stored when it created, recovered or enrolled the identity. Only needed for an identity set up on this device before seeds were stored: run poweur key recover <id> --seed … once to store it
--sas <digits>approveThe six digits the new device shows (typed code only)
--no-waitapproveReturn at once if the new device has not answered yet
caution

There is no way to approve blindly. A typed code needs the digits; a link needs nothing because it is authentic by itself. A mismatch ends the pairing — start again.

The keys the new device receives are saved only if they derive the identity's published key. See Key management & recovery.


poweur key protect / poweur key unprotect​

Encrypt an identity's key files at rest with a passphrase (scrypt + AES-256-GCM), or decrypt them again. The CLI historically wrote plaintext base64 with mode 0600 — defensible for a bot on a hardened host, thin for a laptop.

export POWEUR_KEY_PASSPHRASE='…'
poweur key protect

Encrypted and plaintext files are both readable; detection is by shape, so migration needs no rename or config change. Every command then loads keys transparently as long as POWEUR_KEY_PASSPHRASE is set — the unattended-agent path.

FlagDescription
--passphrase <value>Passphrase (prefer the environment variable so it stays out of shell history)
--use-identity <subdomain>Identity whose keys to protect
--jsonMachine-readable output
caution

Without the passphrase the keys cannot be loaded. Losing it is equivalent to losing the device — recover from the seed or another enrolled device.


poweur contacts <ls|add|request|accept|block|rm> [<identity>]​

Manage the contact list in .poweur/relay/contacts.json. See Contacts & trust.

poweur contacts request bob.poweur.net
poweur contacts accept bob.poweur.net --petname bob
poweur contacts ls
SubcommandEffect
lsList contacts with state and petname
add <id>Add directly as accepted (no request round-trip)
request <id>Send a contact request
accept <id>Accept a pending incoming request
block <id>Block an identity
rm <id>Remove a contact
FlagDescription
--petname <name>Local display name for this contact
--use-identity <subdomain>Override identity

poweur requests​

List pending incoming contact requests. Accept them with poweur contacts accept.

poweur requests --json

poweur policy <show|set>​

Read or write the inbox policy — who may reach you, and on what terms.

poweur policy show
poweur policy set contacts_and_requests --anon-allow=true --anon-challenge=pow --anon-bits=20
ModeEffect
openAnyone may send chat; contact requests still land in the requests queue
contacts_onlyOnly accepted contacts
contacts_and_requestsContacts, plus strangers who may send a contact request
FlagDescription
--anon-allow <bool>Accept anonymous (unsigned) messages
--trusted-auth <id>OAuth bridge allowed to send sign-in prompts (repeatable; none clears). Kept across policy set when omitted
--anon-challenge <kind>none | pow | verified | payment
--anon-bits <n>Proof-of-work difficulty; each +1 doubles sender work (0 = relay default)
--anon-max-bytes <n>Max anonymous payload bytes (0 = default 4096)
--anon-max-per-day <n>Max accepted anonymous messages per day (0 = default 20)
--use-identity <subdomain>Override identity
--jsonMachine-readable output

poweur report <identity>​

Report an identity to the relay operator who hosts them (sys.abuse.report). The report is signed by you and carries message IDs, a reason and an optional note — never message content, which is end-to-end encrypted and which the operator could not read anyway.

poweur report loud.cheapco.test --reason=spam --note="twelve identical messages" --message-ids=m-1,m-2
FlagDescription
--reason <kind>spam | harassment | phishing | malware | impersonation | other
--note <text>Free text for the operator (max 2048 bytes)
--message-ids <ids>Comma-separated message IDs as evidence (max 32)
--use-identity <subdomain>Override identity
--jsonMachine-readable output

One report per reporter per subject per day counts; repeats answer duplicate. A relay only accepts reports about identities it hosts.


poweur blocks <export|import>​

Blocklists are block decisions made portable: a signed document a community can pool. Export publishes shared/blocks.json in your own tree, where a share hands it to a chosen audience. Import verifies the publisher's signature and merges into your own contacts, where you can see and undo it.

poweur blocks export --name "my list"
poweur blocks import alice.example.org --dry-run
poweur blocks import --file list.json
FlagDescription
--name <text>Human label for an exported list
--out <file>Also write the signed document locally
--no-publishDo not write it into your own tree
--file <path>Import from a local file instead of a publisher's tree
--path <tree path>Tree path to read from the publisher (default shared/blocks.json)
--forceAlso block identities you have accepted as contacts
--dry-runShow what would change without writing
--use-identity <subdomain>Override identity
--jsonMachine-readable output

Identities you have accepted as contacts are skipped and named unless --force, and an unsigned or altered list is refused outright.


poweur anon​

Read the anonymous queue — messages accepted under the policy's anonymous block. These are unauthenticated: there is no verified sender and no reply path.

poweur anon --json

To send one, use poweur send <to> <message> --anon; the CLI solves the recipient's proof-of-work challenge if their policy demands one.


poweur group <create|show|add|remove> <group-id>​

Create and administer a group identity — a group with its own Poweur ID, usable in any owner's grants. See Group identities.

poweur group create crew.acme.poweur.net --member bob.example.org
poweur group show crew.acme.poweur.net --json
poweur group add crew.acme.poweur.net --member carol.poweur.net
poweur group remove crew.acme.poweur.net --member bob.example.org
FlagDescription
--member <id>Member Poweur ID (repeatable)
--admin <id>Admin Poweur ID (repeatable); defaults to the active identity on create
--relay <url>Relay to register the group on (create only)
--use-identity <id>Identity performing the operation
--jsonMachine-readable output

create registers the group as a hosted identity, signs its membership document with the group's own key, and restores the active identity. Membership updates bump the group's epoch only when something actually changed, and the last admin cannot be removed. The group name must be a full Poweur ID.


poweur auth <inspect|sign> <request-file-or-url>​

Inspect or approve a third-party sign-in request. inspect shows what is being asked without signing; sign produces the assertion.

poweur auth inspect ./request.json
poweur auth sign ./request.json --json
FlagDescription
--use-identity <subdomain>Override identity
--jsonMachine-readable output

Environment Variables​

VariablePurpose
DNS_SERVEROverride the system resolver for identity lookups. Accepts host, host:port, or an IPv4/IPv6 literal (e.g. 1.1.1.1). Useful when the local network caches negative DNS responses.
DEBUG_HTTPWhen set to a non-empty value, dumps every relay HTTP request and response to stderr. Sensitive material (session and identity signatures) appears in these dumps — only use for local debugging.

Exit Codes​

CodeMeaning
0Success
1General error
2Configuration error (missing or invalid config)
3Network error (relay unreachable)
4Authentication error (signature or challenge failure)
5Rate limit exceeded