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:
| Setting | Env var |
|---|---|
| Relay URL | RELAY_URL |
| Identity subdomain | IDENTITY |
| Keys directory | KEYS_DIR |
| Key-file passphrase | POWEUR_KEY_PASSPHRASE |
Global Flags
All commands accept these global flags:
| Flag | Description |
|---|---|
--json | Output 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/CNAMErouting 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:
| Flag | Description |
|---|---|
--hosted | Hosted 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 |
--json | Machine-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:
- Loads (or creates) a session for the active identity via
ensure session. - Generates a client-side message id (ULID-shaped) and writes a
queuedentry to the local pending journal (~/.poweur/pending/<identity>.jsonl). - 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. - Encrypts the payload with ChaCha20-Poly1305 under an X25519-derived key (see End-to-End Encryption).
- Signs the canonical envelope (including the
id:line and theenc:line) with the session private key. - Attaches
session_idandsession_proofso the recipient's relay can verify without contacting the sender's relay. - Resolves the recipient's home relay via DNS and POSTs
/messagesdirectly there (default direct-send model). On202 Acceptedthe journal advances todelivered_recipient_relay(tick 1).
poweur send bob.example.org "Hey Bob, are you there?"
Flags:
| Flag | Description |
|---|---|
--use-identity <identity> | Send from a specific identity (overrides active identity) |
--via-home-relay | Privacy 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. |
--json | Machine-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:
- Ensures a valid session.
- Fetches a challenge from the relay, signs it with the session key, and calls
GET /messages/:identity. - Decrypts any envelope with
encryptionmetadata using the local X25519 private key. Decrypted messages are prefixed with🔒in human output. - For every successfully decrypted message, signs and POSTs a
delivered_clientack 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. - Drains the response's
acksarray, advancing the local pending journal todelivered_clientfor any referenced message ids — this is how the sender learns about tick 2. - Silently re-registers and retries if the relay reports the session expired.
poweur inbox
Flags:
| Flag | Description |
|---|---|
--use-identity <identity> | Fetch inbox for a specific identity |
--json | Raw JSON output (including undecrypted envelope) |
--decrypt | With --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:
| Glyph | State | Meaning |
|---|---|---|
· | queued | Send pipeline started, no relay response yet |
✓ | delivered_recipient_relay | Recipient relay returned 202 Accepted (tick 1) |
✓✓ | delivered_client | Recipient client acked successful decrypt (tick 2) |
✗ | failed | Send pipeline gave up; sticky |
poweur messages status
poweur messages status --id msg_01j9xkay7g000000000000000
poweur messages status --json
Flags:
| Flag | Description |
|---|---|
--use-identity <identity> | Read the journal for a specific identity |
--id <message_id> | Show only the entry for the given message id |
--json | Machine-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
| Flag | Description |
|---|---|
--grace <duration> | How long the previous key stays valid (default 168h) |
--use-identity <subdomain> | Identity to rotate |
--json | Machine-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
| Flag | Description |
|---|---|
--seed <base64url> | Master seed (required) |
--json | Machine-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.
| Flag | Description |
|---|---|
--seed <base64url> | Master seed (required) |
--relay <url> | Relay to configure (needed on a fresh machine) |
--parent-domain <domain> | Parent domain to configure |
--json | Machine-readable output |
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.
| Flag | Command | Description |
|---|---|---|
--label <text> | enroll | Device description shown to the approver |
--wait | enroll | Keep going until approved, then install the keys |
--seed <value> | approve | Master 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> | approve | The six digits the new device shows (typed code only) |
--no-wait | approve | Return at once if the new device has not answered yet |
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.
| Flag | Description |
|---|---|
--passphrase <value> | Passphrase (prefer the environment variable so it stays out of shell history) |
--use-identity <subdomain> | Identity whose keys to protect |
--json | Machine-readable output |
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
| Subcommand | Effect |
|---|---|
ls | List 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 |
| Flag | Description |
|---|---|
--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
| Mode | Effect |
|---|---|
open | Anyone may send chat; contact requests still land in the requests queue |
contacts_only | Only accepted contacts |
contacts_and_requests | Contacts, plus strangers who may send a contact request |
| Flag | Description |
|---|---|
--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 |
--json | Machine-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
| Flag | Description |
|---|---|
--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 |
--json | Machine-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
| Flag | Description |
|---|---|
--name <text> | Human label for an exported list |
--out <file> | Also write the signed document locally |
--no-publish | Do 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) |
--force | Also block identities you have accepted as contacts |
--dry-run | Show what would change without writing |
--use-identity <subdomain> | Override identity |
--json | Machine-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
| Flag | Description |
|---|---|
--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 |
--json | Machine-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
| Flag | Description |
|---|---|
--use-identity <subdomain> | Override identity |
--json | Machine-readable output |
Environment Variables
| Variable | Purpose |
|---|---|
DNS_SERVER | Override 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_HTTP | When 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
| Code | Meaning |
|---|---|
0 | Success |
1 | General error |
2 | Configuration error (missing or invalid config) |
3 | Network error (relay unreachable) |
4 | Authentication error (signature or challenge failure) |
5 | Rate limit exceeded |