Caution
This CLI is in beta status and ready for you to use. Software in this status may contain bugs or change based on feedback.
A command-line interface for Convos — privacy-focused ephemeral messaging built on XMTP.
- Per-conversation identities: Every conversation gets its own XMTP inbox — no linkability across conversations
- Invite system: Generate QR codes and invite links; join conversations without knowing the creator's address
- Per-conversation profiles: Different display name and avatar in each conversation
- Explode: Permanently destroy a conversation and all its cryptographic keys
- Lock: Prevent new members from being added to a conversation
- Agent mode: Single long-running process for bots — streams messages, auto-processes joins, accepts commands via stdin
- JSON output: Every command supports
--jsonfor scripting and automation
Standard XMTP uses a single identity (wallet + inbox) across all conversations. Convos creates a unique identity per conversation for maximum privacy:
| Property | How |
|---|---|
| No linkability | Conversations cannot be correlated by external observers |
| Isolated keys | Each conversation has its own wallet key, encryption key, and database |
| Ephemeral | Exploding a conversation destroys the cryptographic identity permanently |
| Per-conversation profiles | Different display names and avatars per conversation |
- Node.js >= 22
# npm
npm install -g @xmtp/convos-cli
# pnpm
pnpm add -g @xmtp/convos-cli# npx
npx @xmtp/convos-cli --help
# pnpx
pnpx @xmtp/convos-cli --help
# yarn
yarn dlx @xmtp/convos-cli --help# 1. Initialize configuration
convos init
# 2. Create a conversation (auto-creates a per-conversation identity)
convos conversations create --name "My Group" --profile-name "Alice"
# 3. Send a message
convos conversation send-text <conversation-id> "Hello!"
# 4. Generate an invite QR code for others to join
convos conversation invite <conversation-id>
# 5. List all conversations across all identities
convos conversations list
# 6. Stream messages in real-time
convos conversation stream <conversation-id>Running convos init creates ~/.convos/.env with:
| Variable | Description |
|---|---|
CONVOS_ENV |
Network: local, dev, or production |
CONVOS_UPLOAD_PROVIDER |
Upload provider for attachments (e.g., pinata) |
CONVOS_UPLOAD_PROVIDER_TOKEN |
Authentication token for upload provider |
CONVOS_UPLOAD_PROVIDER_GATEWAY |
Custom gateway URL for upload provider |
Unlike standard XMTP, there is no global wallet key. Each conversation creates its own identity stored in ~/.convos/identities/.
The default environment is dev. Use --env to change it:
convos init --env productionConfiguration is loaded in priority order:
- CLI flags (highest)
--env-file <path>.envin current directory~/.convos/.env(default)
| 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 |
Run convos --help for all commands, or convos <command> --help for details on a specific command.
The agent serve command runs a single long-running process that combines conversation management, message streaming, join request processing, and command handling — purpose-built for AI agents and bots.
# Create a new conversation and start serving
convos agent serve --name "My Bot" --profile-name "Assistant"
# Attach to an existing conversation
convos agent serve <conversation-id>Without agent serve, an agent has to juggle multiple processes:
convos conversation streamfor incoming messagesconvos conversations process-join-requests --watchfor new membersconvos conversation send-textfor each outgoing message (spawning a new process each time)
agent serve replaces all of that with a single process using an ndjson (newline-delimited JSON) protocol on stdin/stdout.
stdout emits one JSON object per line:
| Event | Description | Key Fields |
|---|---|---|
ready |
Session initialized | conversationId, inviteUrl, inboxId |
message |
Incoming message | id, senderInboxId, content, contentType, sentAt |
member_joined |
New member added | inboxId, conversationId |
sent |
Outgoing message confirmed | id, text or type + details |
error |
Something went wrong | message |
stdin accepts one JSON command per line:
| Command | Required Fields | Optional Fields |
|---|---|---|
send |
text |
replyTo (message ID) |
react |
messageId, emoji |
action (add/remove, default add) |
attach |
file (local path) |
mimeType, replyTo |
remote-attach |
url, contentDigest, secret, salt, nonce, contentLength |
filename, scheme |
stop |
— | — |
stderr receives the QR code and diagnostic logs (never interferes with the JSON protocol).
#!/usr/bin/env bash
# Start agent, read events, echo back every message
convos agent serve --name "Echo Bot" --profile-name "🤖 Echo" | \
while IFS= read -r event; do
type=$(echo "$event" | jq -r '.event')
case "$type" in
ready)
echo "Bot ready! Invite: $(echo "$event" | jq -r '.inviteUrl')" >&2
;;
message)
content=$(echo "$event" | jq -r '.content')
msg_id=$(echo "$event" | jq -r '.id')
# Echo the message back as a reply
echo "{\"type\":\"send\",\"text\":\"You said: $content\",\"replyTo\":\"$msg_id\"}"
;;
member_joined)
echo '{"type":"send","text":"Welcome! 👋"}'
;;
esac
done# React to a message
echo '{"type":"react","messageId":"abc123","emoji":"👍"}'
# Remove a reaction
echo '{"type":"react","messageId":"abc123","emoji":"👍","action":"remove"}'# Send a file (≤1MB sent inline, larger files auto-uploaded via provider)
echo '{"type":"attach","file":"./chart.png"}'
# Reply with an attachment
echo '{"type":"attach","file":"./report.pdf","replyTo":"abc123"}'
# Send a pre-uploaded encrypted file
echo '{"type":"remote-attach","url":"https://...","contentDigest":"...","secret":"...","salt":"...","nonce":"...","contentLength":12345}'| 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 only) |
Each conversation has its own XMTP identity (wallet + inbox). Identities are created automatically when you create or join a conversation, but you can also manage them directly.
# List all identities
convos identity list
# Create an identity manually
convos identity create --label "Work Chat" --profile-name "Alice"
# View identity details (connects to XMTP to show inbox ID)
convos identity info <identity-id>
# Remove an identity (destroys all keys — irreversible)
convos identity remove <identity-id> --force# Create a conversation (auto-creates per-conversation identity)
convos conversations create --name "Project Team" --profile-name "Alice"
# Create with admin-only permissions
convos conversations create --name "Announcement Channel" --permissions admin-only
# List all conversations across all identities
convos conversations list --sync
# Sync all conversations from the network
convos conversations syncConvos uses a serverless invite system. The creator generates a cryptographic invite slug; the joiner sends a DM join request; the creator's client processes it and adds them to the group.
# Generate an invite — displays a QR code in the terminal
convos conversation invite <conversation-id>
# Invite that expires in 1 hour
convos conversation invite <conversation-id> --expires-in 3600
# Single-use invite
convos conversation invite <conversation-id> --single-use
# JSON output (suppresses QR code)
convos conversation invite <conversation-id> --json# Join using a raw invite slug
convos conversations join <invite-slug>
# Join using a full invite URL
convos conversations join "https://dev.convos.org/v2?i=<slug>"
# Join with a display name
convos conversations join <slug> --profile-name "Bob"
# Send join request without waiting for acceptance
convos conversations join <slug> --no-wait
# Wait up to 2 minutes
convos conversations join <slug> --timeout 120The creator's client must be running to process incoming join requests:
# Process all pending join requests
convos conversations process-join-requests
# Continuously watch for join requests
convos conversations process-join-requests --watch
# Process for a specific conversation only
convos conversations process-join-requests --conversation <id># Send different message types
convos conversation send-text <id> "Hello!"
convos conversation send-reaction <id> <message-id> add "👍"
convos conversation send-reply <id> <message-id> "I agree!"
# Read messages
convos conversation messages <id> --sync --limit 10
# Stream messages in real-time
convos conversation stream <id>
convos conversation stream <id> --timeout 60# Send a photo (small files ≤1MB sent inline)
convos conversation send-attachment <id> ./photo.jpg
# Large files are automatically encrypted and uploaded via configured provider
convos conversation send-attachment <id> ./video.mp4
# Force remote upload even for small files
convos conversation send-attachment <id> ./photo.jpg --remote
# Override MIME type
convos conversation send-attachment <id> ./file.bin --mime-type image/png
# Per-command upload provider (no .env needed)
convos conversation send-attachment <id> ./photo.jpg \
--upload-provider pinata --upload-provider-token <jwt>
# Encrypt only (for manual upload workflows)
convos conversation send-attachment <id> ./photo.jpg --encrypt
# Send a pre-uploaded encrypted file
convos conversation send-remote-attachment <id> <url> \
--content-digest <hex> --secret <base64> --salt <base64> \
--nonce <base64> --content-length <bytes>
# Download an attachment (handles both inline and remote transparently)
convos conversation download-attachment <id> <message-id>
# Download to a specific path
convos conversation download-attachment <id> <message-id> --output ./photo.jpg
# Reply with a photo
convos conversation send-reply <id> <message-id> --file ./photo.jpgTo configure an upload provider for large files, add to your ~/.convos/.env:
CONVOS_UPLOAD_PROVIDER=pinata
CONVOS_UPLOAD_PROVIDER_TOKEN=<your-pinata-jwt>
# Optional: custom gateway URL
CONVOS_UPLOAD_PROVIDER_GATEWAY=https://your-gateway.mypinata.cloudSupported upload providers: pinata
Each conversation has independent profiles — you can be a different person in each conversation. Profiles are stored in the group's metadata and visible to all members.
# Set your display name in a conversation
convos conversation update-profile <id> --name "Alice"
# Set name and avatar
convos conversation update-profile <id> --name "Alice" --image "https://example.com/avatar.jpg"
# Go anonymous (clear profile)
convos conversation update-profile <id> --name "" --image ""
# View all member profiles
convos conversation profiles <id>
convos conversation profiles <id> --json# View members
convos conversation members <id>
# Add/remove members
convos conversation add-members <id> <inbox-id>
convos conversation remove-members <id> <inbox-id>
# Update metadata
convos conversation update-name <id> "New Name"
convos conversation update-description <id> "New description"
# View permissions
convos conversation permissions <id>Prevent new members from being added:
convos conversation lock <id>
convos conversation lock <id> --unlockPermanently destroy a conversation and all its cryptographic keys:
# Explode immediately
convos conversation explode <id> --force
# Schedule explosion for a future date
convos conversation explode <id> --scheduled "2025-03-01T00:00:00Z"Exploding sends an ExplodeSettings notification to all members (so iOS and other clients trigger their cleanup flow), updates group metadata with the expiration timestamp, removes all members, then destroys the local identity. The conversation becomes unreadable.
When using --scheduled, members are notified but not removed — clients handle cleanup when the time arrives.
All commands support --json for machine-readable output:
# Create a conversation and capture the ID
CONV_ID=$(convos conversations create --name "Test" --json | jq -r '.conversationId')
# Send a message
convos conversation send-text "$CONV_ID" "Hello!"
# Read messages as JSON
convos conversation messages "$CONV_ID" --sync --json
# Generate invite and capture the URL
INVITE_URL=$(convos conversation invite "$CONV_ID" --json | jq -r '.url')Use --verbose to see detailed client initialization info. When combined with --json, verbose logs go to stderr:
convos identity info <id> --verbose~/.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
┌──────────────────────────────────────────┐
│ @xmtp/convos-cli │
│ │
│ Commands: │
│ agent serve (long-running bot mode) │
│ identity create/list/info/remove │
│ conversations create/join/list/sync │
│ conversation invite/explode/lock │
│ conversation send-text/stream/... │
│ conversation send-attachment/download │
│ conversation update-profile/profiles │
│ │
│ ┌────────────────────────────────────┐ │
│ │ @xmtp/node-sdk │ │
│ │ │ │
│ │ Per-conversation XMTP clients │ │
│ │ Group/DM management │ │
│ │ Message encryption & delivery │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
@xmtp/convos-cli exports its core functionality for use in other applications:
import {
createIdentityStore,
createClientForIdentity,
createInviteSlug,
parseInvite,
verifyInvite,
parseAppData,
serializeAppData,
upsertProfile,
ConvosBaseCommand,
} from "@xmtp/convos-cli";See the exports for the full API.
This package includes an agent skill (skills/convos-cli/SKILL.md) that teaches AI coding agents how to use the Convos CLI.
Claude Code
Add the skill directory to your project's .claude/settings.json:
{
"skills": ["./node_modules/@xmtp/convos-cli/skills"]
}Other agents (Cursor, Windsurf, Codex, etc.)
Use openskills to install the skill:
npx openskills install ./node_modules/@xmtp/convos-cli/skillsOr point your agent to node_modules/@xmtp/convos-cli/skills/convos-cli/SKILL.md directly.
# Run all tests (fast, no network required)
npm test
# Watch mode
npm run test:watch# Install dependencies
npm install
# Build TypeScript
npm run build
# Dev mode (tsx, no build needed)
npm run dev -- conversations list
# Run built version
npm start -- conversations list
# Type check without emitting
npm run typecheck