| name | convos-cli |
| description | Use when working with Convos messaging - privacy-focused ephemeral messaging with per-conversation identities, invites, profiles, and group management via the convos CLI tool |
Convos CLI
The Convos CLI (convos) is a command-line tool for privacy-focused ephemeral messaging built on XMTP. Unlike standard XMTP, Convos creates a unique identity per conversation so conversations cannot be linked or correlated.
Key differences from standard XMTP:
- Per-conversation identities: Each conversation gets its own wallet, inbox, and database
- No global wallet key: Identities are created automatically when creating/joining conversations
- Invite system: Serverless QR code + URL invites for joining conversations
- Per-conversation profiles: Different display name and avatar in each conversation
- Explode: Permanently destroy a conversation and all cryptographic keys
- Lock: Prevent new members from being added
Prerequisites
Initialize Configuration
convos init
convos init --stdout
convos init --env production
convos init --force
convos init --home /path/to/agent1-data
CONVOS_HOME=/path/to/agent1-data convos init
This creates a .env file with:
CONVOS_ENV - Network environment (local, dev, production)
CONVOS_API_KEY - Agent API key for uploads (auto-selects convos-api provider)
CONVOS_UPLOAD_PROVIDER - Upload provider override (convos-api, pinata, s3)
Note: Unlike standard XMTP, there is no global wallet key. Each conversation creates its own identity stored in ~/.convos/identities/.
Custom Data Directory
By default, all data is stored in ~/.convos/. To use a different directory (e.g., when running multiple agents on one machine), use the --home flag or the CONVOS_HOME environment variable:
convos conversations list --home /path/to/agent-data
export CONVOS_HOME=/path/to/agent-data
convos conversations list
Priority: --home flag > CONVOS_HOME env var > ~/.convos
Configuration Loading Priority
- CLI flags (highest priority)
- Explicit
--env-file <path>
.env in the current working directory
<convos-home>/.env (default: ~/.convos/.env)
Command Structure
convos [TOPIC] [COMMAND] [ARGUMENTS] [FLAGS]
Topics
| Topic | Purpose |
|---|
agent | Agent mode — long-running sessions with streaming I/O |
identity | Manage per-conversation identities (inboxes) |
conversations | List, create, join, and stream conversations |
conversation | Interact with a specific conversation |
Standalone Commands
| Command | Purpose |
|---|
init | Initialize configuration and directory structure |
reset | Delete all identities and conversation data (preserves .env) |
schema | Introspect CLI commands as machine-readable JSON (args, flags, examples) |
Output Modes
All commands support --json for machine-readable JSON output:
convos conversations list --json
Use --fields to limit JSON output to specific fields (implicitly enables --json). Supports dot notation for nested paths:
convos conversation messages <id> --fields id,content,senderInboxId
convos conversation messages <id> --fields id,content,contentType.typeId,sentAt
convos conversation profiles <id> --fields profiles
convos conversations list --fields conversationId,name
Use --verbose to see detailed client initialization logs. When combined with --json, verbose output goes to stderr:
convos identity info <id> --verbose
convos conversations list --json --verbose 2>/dev/null
Common Workflows
Create a Conversation
convos conversations create --name "My Group" --profile-name "Alice"
convos conversations create --name "Announcement Channel" --permissions admin-only
CONV_ID=$(convos conversations create --name "Test" --json | jq -r '.conversationId')
Send Messages
convos conversation send-text <conversation-id> "Hello, world!"
convos conversation send-reaction <conversation-id> <message-id> add "👍"
convos conversation send-reaction <conversation-id> <message-id> remove "👍"
convos conversation send-reply <conversation-id> <message-id> "Replying to you"
convos conversation send-reply <conversation-id> <message-id> --file ./photo.jpg
convos conversation send-reply <conversation-id> <message-id> --file ./video.mp4
convos conversation send-read-receipt <conversation-id>
convos conversation last-read-times <conversation-id>
convos conversation last-read-times <conversation-id> --sync --json
convos conversation send-typing-indicator <conversation-id>
convos conversation send-typing-indicator <conversation-id> --stop
Send Attachments
convos conversation send-attachment <conversation-id> ./photo.jpg
convos conversation send-attachment <conversation-id> ./photo.jpg --remote
convos conversation send-attachment <conversation-id> ./file.bin --mime-type image/png
convos conversation send-attachment <conversation-id> ./photo.jpg \
--upload-provider pinata --upload-provider-token <jwt>
convos conversation send-attachment <conversation-id> ./photo.jpg --encrypt
convos conversation send-remote-attachment <conversation-id> <url> \
--content-digest <hex> --secret <base64> --salt <base64> \
--nonce <base64> --content-length <bytes> --filename photo.jpg
convos conversation download-attachment <conversation-id> <message-id>
convos conversation download-attachment <conversation-id> <message-id> --output ./photo.jpg
convos conversation download-attachment <conversation-id> <message-id> --raw
To enable automatic upload for large files, set your agent API key in .env:
CONVOS_API_KEY=<your-agent-api-key>
This auto-selects the convos-api upload provider. Other providers (pinata, s3) are also available via CONVOS_UPLOAD_PROVIDER.
Read Messages
convos conversation messages <conversation-id>
convos conversation messages <conversation-id> --sync --limit 10
Stream Messages in Real-Time
convos conversation stream <conversation-id>
convos conversation stream <conversation-id> --timeout 60
List Conversations
convos conversations list
convos conversations list --sync
Invite System
Convos uses a serverless invite system. The creator generates a cryptographic invite URL; the person joining must open the URL in the Convos app (or scan the QR code); then the creator processes the join request to add them to the group.
Important: Adding someone to a conversation is a multi-step process:
- Generate an invite (creator side) — produces a URL and QR code
- Person opens the invite URL in Convos or scans the QR code — this sends a join request to the creator via DM
- Creator processes the join request — this validates the request and adds the person to the group
The creator must process join requests after the person has opened/scanned the invite. If you don't know when that will happen, use --watch with a timeout to stream and process requests as they arrive.
Inspect an Invite
convos conversations inspect-invite <invite-slug>
convos conversations inspect-invite "https://dev.convos.org/v2?i=<slug>"
convos conversations inspect-invite <slug> --json
This displays the invite's tag, creator inbox ID, conversation name, expiration dates, signature validity, and whether the invite is expired — without creating any identities or sending join requests.
Create an Invite
convos conversation invite <conversation-id>
convos conversation invite <conversation-id> --expires-in 3600
convos conversation invite <conversation-id> --single-use
convos conversation invite <conversation-id> --json
INVITE_URL=$(convos conversation invite <conversation-id> --json | jq -r '.url')
Person Joins via Invite
The person being invited must open the invite URL in the Convos app or scan the QR code with Convos. This can be done:
- On iOS: Open the URL in Safari (redirects to Convos app) or scan the QR code from within the app
- Via CLI: Use
convos conversations join
convos conversations join <invite-slug>
convos conversations join "https://dev.convos.org/v2?i=<slug>"
convos conversations join <slug> --profile-name "Bob"
convos conversations join <slug> --profile-name "Bot" --profile-image "https://example.com/avatar.jpg"
convos conversations join <slug> --metadata role=assistant --metadata version=2
convos conversations join <slug> --no-wait
convos conversations join <slug> --timeout 120
Process Join Requests (Creator Side)
After the person has opened/scanned the invite, the creator must process the join request:
convos conversations process-join-requests
convos conversations process-join-requests --conversation <id>
convos conversations process-join-requests --watch --conversation <id>
convos conversations process-join-requests --watch
Per-Conversation Profiles
Each conversation has independent profiles — you can have a different name and avatar in each.
Profile updates are sent as ProfileUpdate messages to the group. The CLI no longer writes profiles to appData (this was removed to fix a data corruption bug where concurrent read-modify-write cycles could erase invite tags and other members' profiles). When reading profiles, message-sourced profiles take precedence, with appData as a read-only fallback for profiles written by older clients (e.g., iOS).
When new members are added (via invite or directly), a ProfileSnapshot message is sent containing all current member profiles so the new joiner has everyone's data immediately — solving the MLS forward secrecy problem where older messages may be undecryptable.
Profile resolution precedence:
- Latest ProfileUpdate from that member — highest priority, most recent self-authored update
- Most recent ProfileSnapshot containing that member — fallback when no ProfileUpdate exists
- appData profiles — legacy fallback for backward compatibility with older clients
- No profile — member has no name/avatar set
Both ProfileUpdate and ProfileSnapshot are silent messages (shouldPush = false) — they do not appear in chat or trigger notifications.
convos conversation update-profile <conversation-id> --name "Alice"
convos conversation update-profile <conversation-id> --name "Alice" --image "https://example.com/avatar.jpg"
convos conversation update-profile <conversation-id> --name "" --image ""
convos conversation profiles <conversation-id>
convos conversation profiles <conversation-id> --json
Identity Management
Identities are created automatically when creating/joining conversations, but you can manage them directly.
convos identity list
convos identity create --label "Work Chat" --profile-name "Alice"
convos identity info <identity-id>
convos identity remove <identity-id> --force
Reset All Data
Delete all identities and conversation data. The .env configuration is preserved.
convos reset
convos reset --force
Group Management
convos conversation members <conversation-id>
convos conversation add-members <conversation-id> <inbox-id>
convos conversation remove-members <conversation-id> <inbox-id>
convos conversation update-name <conversation-id> "New Name"
convos conversation update-description <conversation-id> "New description"
convos conversation permissions <conversation-id>
Lock a Conversation
Prevent new members from joining by setting the addMember permission to deny. This also invalidates all existing invites. Only super admins can lock/unlock.
convos conversation lock <conversation-id>
convos conversation lock <conversation-id> --unlock
Explode a Conversation
Permanently destroy a conversation and all its cryptographic keys. Sends an ExplodeSettings notification to all members (so iOS and other clients can trigger their cleanup), updates group metadata with the expiration timestamp, removes all members, then destroys the local identity. Irreversible.
convos conversation explode <conversation-id> --force
convos conversation explode <conversation-id> --scheduled "2025-03-01T00:00:00Z"
When scheduled, the ExplodeSettings message is sent with a future expiresAt date. Members are notified but not removed — clients handle cleanup when the time arrives. When immediate (no --scheduled), members are removed and the local identity is destroyed right away.
Assistant Attestation
Cryptographically verify that an agent was provisioned by the Convos backend. Attestations use Ed25519 signatures over sha256(inboxId || timestamp).
convos attestation generate <inbox-id>
convos attestation generate <inbox-id> --kid my-key-2026 --json
convos attestation verify <inbox-id> \
--attestation <base64url-sig> \
--attestation-ts <iso8601> \
--attestation-kid <kid>
convos attestation verify <inbox-id> \
--attestation <sig> \
--attestation-ts <ts> \
--public-key <base64url-pubkey>
convos attestation verify <inbox-id> \
--attestation <sig> \
--attestation-ts <ts> \
--attestation-kid <kid> \
--jwks-file ./agents.json
Agents include attestation in their profile metadata when joining:
convos agent serve --name "Bot" \
--attestation <sig> \
--attestation-ts <ts> \
--attestation-kid <kid>
CONVOS_ATTESTATION=<sig> CONVOS_ATTESTATION_TS=<ts> CONVOS_ATTESTATION_KID=<kid> \
convos agent serve --name "Bot"
convos conversations join <slug> \
--attestation <sig> \
--attestation-ts <ts> \
--attestation-kid <kid>
Sync Data from Network
convos conversations sync
convos conversation sync <conversation-id>
Agent Mode
The agent serve command runs a long-running process that combines conversation creation, message streaming, join request processing, and stdin command handling — ideal for AI agents and bots.
Quick Start (Agent)
convos agent serve --name "My Bot" --profile-name "Assistant"
convos agent serve <conversation-id>
convos agent serve --name "Agent" --permissions admin-only
Protocol
The agent uses an ndjson (newline-delimited JSON) protocol:
- stdout: Events (one JSON object per line)
- stdin: Commands (one JSON object per line)
- stderr: QR code + diagnostic logs
Events (stdout)
| Event | Description | Key Fields |
|---|
ready | Session started | conversationId, inviteUrl, inboxId |
message | New message received | id, senderInboxId, senderProfile (optional: name, image), content, contentType, sentAt, catchup (optional) |
typing | Member typing status changed | senderInboxId, isTyping, conversationId |
member_joined | Member joined via invite | inboxId, conversationId, catchup (optional) |
sent | Message sent confirmation | id, text, replyTo (optional), type (optional) |
heartbeat | Periodic health check | conversationId, activeStreams |
error | Error occurred | message |
Messages with catchup: true were fetched during stream reconnection (missed while disconnected).
Commands (stdin)
{"type":"send","text":"Hello, world!"}
{"type":"send","text":"Replying to you","replyTo":"<message-id>"}
{"type":"react","messageId":"<message-id>","emoji":"👍"}
{"type":"react","messageId":"<message-id>","emoji":"👍","action":"remove"}
{"type":"attach","file":"./photo.jpg"}
{"type":"attach","file":"./photo.jpg","replyTo":"<message-id>"}
{"type":"attach","file":"./photo.jpg","mimeType":"image/jpeg"}
{"type":"remote-attach","url":"https://...","contentDigest":"<hex>","secret":"<base64>","salt":"<base64>","nonce":"<base64>","contentLength":12345,"filename":"photo.jpg"}
{"type":"rename","name":"New Group Name"}
{"type":"read-receipt"}
{"type":"typing"}
{"type":"typing","isTyping":false}
{"type":"lock"}
{"type":"unlock"}
{"type":"explode"}
{"type":"explode","scheduled":"2025-03-01T00:00:00Z"}
{"type":"stop"}
| Command | Required Fields | Optional Fields |
|---|
send | text | replyTo |
react | messageId, emoji | action (add/remove, default: add) |
attach | file (local path) | mimeType, replyTo |
remote-attach | url, contentDigest, secret, salt, nonce, contentLength | filename, scheme |
rename | name | — |
read-receipt | — | — |
typing | — | isTyping (bool, default: true) |
lock | — | — |
unlock | — | — |
explode | — | scheduled (ISO8601 date) |
stop | — | — |
Small attachments (≤1MB) are sent inline. Larger files are auto-encrypted and uploaded via the configured upload provider (e.g., Pinata).
Lock prevents new members from joining by rotating the invite tag and setting addMember permission to deny. Unlock reverses this (previously shared invites remain invalid). Explode permanently destroys the conversation — sends ExplodeSettings notification, removes members, and deletes the local identity. Immediate explode triggers agent shutdown. Rename updates the conversation name visible to all members.
How It Works
When started, agent serve:
- Creates or attaches to a conversation
- Displays QR code invite on stderr (so users can scan and join)
- Emits
ready event with conversation ID, invite URL, and identity info
- Processes pending join requests from before the agent started
- Streams messages — emits
message events as they arrive in real-time
- Streams DM join requests — automatically adds new members and emits
member_joined
- Reads stdin — accepts
send, rename, lock, unlock, explode, and stop commands
- Emits heartbeat (optional) — periodic health check events when
--heartbeat is set
- Catches up on reconnect — if a stream disconnects and reconnects, fetches any missed messages since the last seen timestamp
All of these run concurrently. The agent stays alive until SIGINT, SIGTERM, stdin close, a stop command, or an immediate explode.
Example: Agent Integration
convos agent serve --name "Bot" --profile-name "AI Assistant" | while IFS= read -r event; do
type=$(echo "$event" | jq -r '.event')
case "$type" in
ready)
echo "Bot ready! Invite URL: $(echo "$event" | jq -r '.inviteUrl')" >&2
;;
message)
content=$(echo "$event" | jq -r '.content')
echo "Received: $content" >&2
msg_id=$(echo "$event" | jq -r '.id')
echo "{\"type\":\"send\",\"text\":\"You said: $content\",\"replyTo\":\"$msg_id\"}"
;;
member_joined)
inbox=$(echo "$event" | jq -r '.inboxId')
echo "New member: $inbox" >&2
echo "{\"type\":\"send\",\"text\":\"Welcome!\"}"
;;
esac
done
Agent Flags
| Flag | Description |
|---|
--name | Conversation name (when creating new) |
--description | Conversation description (when creating new) |
--permissions | all-members or admin-only (when creating new) |
--profile-name | Display name for this conversation |
--identity | Use an existing unlinked identity |
--label | Local label for the identity |
--no-invite | Skip generating an invite (attach mode) |
--heartbeat | Emit heartbeat events every N seconds (0 to disable, default: 0) |
Important Concepts
Per-Conversation Identities
Every conversation has its own:
- Wallet key (secp256k1 private key)
- DB encryption key (32-byte key)
- XMTP inbox (unique inbox ID)
- Local database (SQLite)
Identities are stored in <convos-home>/identities/<id>.json. Databases are stored in <convos-home>/db/<env>/<id>.db3. The data directory defaults to ~/.convos/ but can be overridden with --home or CONVOS_HOME.
Invite Flow
- Creator generates an invite URL/QR code (contains encrypted conversation token + creator's inbox ID)
- Person opens the invite URL in Convos app (or scans the QR code) — this creates a per-conversation identity and sends a DM join request to the creator
- Creator processes the join request — validates the invite signature, decrypts the conversation token, and adds the person to the group
- Person is now a member with their own isolated identity
Key point: Step 3 must happen after step 2. The creator must either run process-join-requests after the invite has been opened, or use --watch to stream and process requests as they arrive.
Profile Messages
Member profiles are stored as XMTP group messages using two custom content types:
ProfileUpdate (convos.org/profile_update:1.0) — sent by a member when they change their own name or avatar. The sender's inbox ID is implicit from the XMTP message, preventing spoofing.
ProfileSnapshot (convos.org/profile_snapshot:1.0) — sent after adding members to a group. Contains all current member profiles so new joiners have data immediately (solves MLS forward secrecy gap).
Both are silent (no push notification, not displayed in chat). The CLI reads appData profiles as a fallback for backward compatibility with older clients, but does not write profiles there. Custom XMTP content codecs (ProfileUpdateCodec, ProfileSnapshotCodec) are registered with the XMTP client at creation time so the SDK can decode these message types natively.
Profiles support typed metadata — arbitrary key-value pairs where values can be string, number (double), or boolean. Metadata is carried in both ProfileUpdate and ProfileSnapshot messages via a map<string, MetadataValue> protobuf field. Use --metadata key=value on update-profile (repeatable, auto-typed: "true"/"false" → bool, numeric → number, else string). Metadata merges with existing values (new keys overwrite, unmentioned keys preserved).
Profile images are encrypted end-to-end using the same scheme as iOS: HKDF-SHA256 derives a per-image AES-256-GCM key from the group's imageEncryptionKey (stored in appData) + random 32-byte salt, then encrypts with a random 12-byte nonce. The encrypted blob is uploaded via the configured upload provider and the URL + salt + nonce are sent as EncryptedProfileImageRef in the ProfileUpdate message. If no imageEncryptionKey exists for the group, the CLI generates one and writes it to appData.
Supported upload providers:
- Convos API:
CONVOS_UPLOAD_PROVIDER=convos-api, CONVOS_API_KEY=<agent-assets-api-key>, optional CONVOS_API_BASE_URL=<url> (auto-derived from XMTP env: dev → https://api.dev.convos.xyz/api, production → https://api.prod.convos.xyz/api). Uses the agent asset upload endpoint (GET /v2/agents/assets/presigned) with X-Agent-API-Key header auth — no JWT step needed.
- Pinata (IPFS):
CONVOS_UPLOAD_PROVIDER=pinata, CONVOS_UPLOAD_PROVIDER_TOKEN=<jwt>, optional CONVOS_UPLOAD_PROVIDER_GATEWAY=<url>
- S3 (direct):
CONVOS_UPLOAD_PROVIDER=s3, CONVOS_UPLOAD_PROVIDER_TOKEN=<accessKeyId>:<secretAccessKey>, CONVOS_S3_BUCKET=<bucket>, optional CONVOS_S3_REGION=<region> (default: us-east-1), optional CONVOS_S3_ENDPOINT=<url> (for S3-compatible services like MinIO, R2), optional CONVOS_UPLOAD_PROVIDER_GATEWAY=<public-url-prefix>
Join Request Messages
Join requests use a structured content type instead of plain text:
JoinRequest (convos.org/join_request:1.0) — sent as a DM to the conversation creator when joining via invite. Contains the invite slug, joiner's profile (name, image, memberKind), and optional metadata.
The CLI sets memberKind: "agent" by default on all join requests so the creator knows a bot is joining. For backward compatibility, the CLI sends both the JoinRequestContent message and a plain text slug — older clients that don't understand the new content type will read the text fallback. When processing incoming join requests, the CLI tries JoinRequestContent first, then falls back to plain text.
Consent States
| State | Meaning |
|---|
allowed | Messages are welcome |
denied | Messages are blocked |
unknown | No decision made |
Environment Networks
| Network | Use Case |
|---|
local | Local XMTP node |
dev | Development/testing (default) |
production | Production use |
Data Directory
The data directory defaults to ~/.convos/ but can be overridden with --home or CONVOS_HOME:
<convos-home>/ # default: ~/.convos/
├── .env # Global config (env only)
├── identities/
│ ├── <id-1>.json # Identity: wallet key, db key, conversation link
│ └── <id-2>.json
└── db/
└── dev/ # XMTP databases by environment
├── <id-1>.db3
└── <id-2>.db3
Error Handling
- Not initialized: Run
convos init to create configuration
- No identities: Create a conversation or identity first
- Identity not found: Use
convos identity list to see available identities
- Conversation not found: Sync first with
convos conversations sync
- Permission denied: Check group permissions with
convos conversation permissions
- Invite expired or invalid: Generate a new invite with
convos conversation invite
Complete Example
convos init --env dev
CONV=$(convos conversations create --name "Project Team" --profile-name "Alice" --json)
CONV_ID=$(echo "$CONV" | jq -r '.conversationId')
convos conversation invite "$CONV_ID"
convos conversations process-join-requests --conversation "$CONV_ID"
convos conversation send-text "$CONV_ID" "Welcome to the team!"
convos conversation stream "$CONV_ID" --timeout 300
Tips
- Always display the full QR code: The
conversation invite and conversations create commands output a scannable QR code rendered in Unicode block characters followed by the invite URL. When showing the user the result, you must display the complete, unmodified command output so the QR code renders correctly in the terminal. Do not summarize, truncate, or omit the QR code — it is the primary way users share invites. Always show the full stdout output to the user. When running agent serve, the QR code is saved as a PNG file (path in the qrCodePath field of the ready event) — display it to the user using the read tool so they can scan it.
- Never use markdown in messages: Convos does not render markdown. When sending messages (via
send-text, send-reply, or agent send commands), always use plain text. Do not use markdown formatting like **bold**, *italic*, # headings, `code`, [links](url), or bullet lists with - or * . Write naturally in plain text instead.
- Identities are automatic: You rarely need to manage them directly — creating/joining conversations handles it
- Use JSON output for scripting: Add
--json flag when extracting data programmatically
- Use
--fields to limit output: When fetching messages or other large responses, use --fields to include only the fields you need — this saves context window tokens and reduces noise. e.g. --fields id,content,senderInboxId
- Sync before reading: Add
--sync flag when reading messages to ensure fresh data
- Process join requests after invite is opened: After generating an invite, wait for the person to open/scan it, then run
process-join-requests. If you don't know when they'll open it, use --watch to stream requests as they arrive
- Lock before exploding: Lock a conversation first to prevent new joins, then explode when ready
- Dangerous operations require --force: Commands like
explode, identity remove, and lock prompt for confirmation unless --force is passed
- Check command help: Run
convos <command> --help for full flag documentation
- Use
convos schema for runtime introspection: convos schema lists all commands as JSON, convos schema <command> shows full args/flags/examples for a specific command. Useful for discovering capabilities without pre-loaded docs