A command-line tool for E2E encrypted messaging over Nostr using the Marmot Protocol (MLS). Compatible with the Whitenoise app.
Built for AI agents who need secure messaging without a GUI — but works for anyone.
⚠️ Note (Feb 2026): JeffG (Marmot Protocol creator) has announced a new version of Whitenoise is coming with improved security and usability. This CLI will be updated for compatibility when the new version drops.
- Whitenoise is the leading E2E encrypted messenger on Nostr, but it's GUI-only (Flutter app)
- AI agents need CLI tools to communicate securely
- marmot-cli bridges this gap — same protocol, no GUI required
You can message Whitenoise users from the command line, and they can message you back.
- 🔒 End-to-end encrypted using MLS (Messaging Layer Security)
- 🔄 Forward secrecy — past messages stay encrypted even if keys leak
- 📱 Whitenoise compatible — chat with Whitenoise app users
- 🤖 Agent-friendly — designed for autonomous AI agents
- 🌐 Decentralized — uses Nostr relays, no central server
- 🆔 Nostr identity — uses your existing Nostr keypair
- 🔐 NIP-46 remote signing — keep your nsec in a bunker, sign remotely (new in v0.2)
- Rust toolchain (1.90.0+)
- A Nostr keypair (nsec) or a NIP-46 bunker URI
git clone https://github.com/kai-familiar/marmot-cli.git
cd marmot-cli
cargo build --releaseThe raw binary (target/release/marmot-cli) generates a random keypair if no credentials are provided. This causes MLS group state issues. Always use the wrapper script or set NOSTR_NSEC.
# Option 1: Use the wrapper script (recommended)
# Put your credentials in .credentials/nostr.json:
echo '{"nsec": "nsec1..."}' > .credentials/nostr.json
./marmot whoami
# Option 2: Set environment variable
export NOSTR_NSEC="nsec1..."
./target/release/marmot-cli whoamiThen publish your key package:
./marmot publish-key-packageInstead of exposing your nsec directly, use a NIP-46 remote signer (bunker):
# Initialize with bunker URI
marmot-cli init --bunker "bunker://<signer-pubkey>?relay=wss://relay.nsec.app&secret=YOUR_TOKEN"
# The bunker connection is stored automatically — no nsec needed!
marmot-cli whoami # Shows "signer: NIP-46 bunker"
marmot-cli publish-key-packageCompatible bunkers: nsecbunkerd, Amber (Android), Nostr Keyguard
Migrating from nsec to bunker:
# Atomic migration — verifies identity match, preserves MLS group state
marmot-cli migrate-to-bunker --bunker "bunker://..."
# Then remove nsec from your environment
unset NOSTR_NSEC# Start a chat with someone (they need a key package published)
./target/release/marmot-cli create-chat npub1... --name "My Chat"# List your chats
./target/release/marmot-cli list-chats
# Send a message (use the MLS Group ID from list-chats)
./target/release/marmot-cli send -g <group-id-prefix> "Hello!"
# Check for new messages
./target/release/marmot-cli receive
# Listen continuously
./target/release/marmot-cli listen --interval 5If someone creates a chat with you from Whitenoise:
# Check for incoming invites
./target/release/marmot-cli receive
# Accept a pending welcome
./target/release/marmot-cli accept-welcome <event-id>| Command | Description |
|---|---|
init --bunker "bunker://..." |
Initialize with NIP-46 bunker (recommended) |
init --nsec "nsec1..." |
Initialize with direct nsec |
whoami |
Show your Nostr identity and signing mode |
publish-key-package |
Publish MLS key package to relays (do this first!) |
create-chat <npub> |
Create a new encrypted chat |
list-chats |
List all your chats |
send -g <id> "msg" |
Send an encrypted message |
receive |
Fetch and process new messages |
accept-welcome <id> |
Accept a group invitation |
listen |
Continuously poll for messages (supports --on-message callback) |
fetch-key-package <npub> |
Check if someone has a key package |
migrate-to-bunker |
Atomically migrate from nsec to bunker signing |
signer-status |
Show current signing mode and bunker connection info |
-n, --nsec <NSEC> Nostr private key (or set NOSTR_NSEC env var)
-b, --bunker <URI> NIP-46 bunker URI (or set NOSTR_BUNKER env var)
-d, --db <DB> Database path [default: ~/.marmot-cli/marmot.db]
-r, --relays <RELAYS> Relay URLs, comma-separated
-q, --quiet Suppress relay connection logs
Process incoming messages in real-time with your own scripts:
# Run a script for each message received
./marmot listen --on-message 'node process-dm.js'Each message is passed as JSON on stdin:
{
"message_id": "abc123...",
"group_id": "62f88693...",
"group_name": "Kai & Jeroen",
"sender": "npub1qffq63l...",
"sender_hex": "024c0d4f...",
"content": "Hello!",
"timestamp": 1770505735,
"is_me": false
}Example handler (process-dm.js):
import { createInterface } from 'readline';
const rl = createInterface({ input: process.stdin });
rl.on('line', (line) => {
const msg = JSON.parse(line);
if (!msg.is_me) {
console.log(`[${msg.group_name}] ${msg.sender}: ${msg.content}`);
// Your logic here: auto-reply, log, forward, etc.
}
});Notes:
- Own messages (
is_me: true) are passed to the callback but you can filter them - The callback runs for every message, including historical ones on first sync
- Exit codes are logged but don't affect the listen loop (yet)
marmot-cli is designed to be used by AI agents running on OpenClaw or similar platforms.
The included ./marmot wrapper script handles credential loading automatically:
- Reads
NOSTR_NSECfrom.credentials/nostr.jsonin your workspace - Adds
-q(quiet) flag to suppress relay logs - Uses the correct binary path
Example:
# Using the wrapper (recommended)
./marmot whoami
./marmot receive
./marmot send -g <group-id> "Hello!"Custom wrapper for different credential locations:
#!/bin/bash
export NOSTR_NSEC=$(cat /path/to/credentials.json | jq -r '.nsec')
exec marmot-cli -q "$@"Check for messages during heartbeats:
# In your heartbeat routine
marmot-cli -q receiveSend messages programmatically:
marmot-cli -q send -g <group-id> "Status update: all systems operational"Use --on-message for reactive agents:
# Forward E2E messages to your agent's inbox
./marmot listen --on-message './forward-to-agent.sh'#!/bin/bash
# forward-to-agent.sh — relay messages to OpenClaw
read JSON
CONTENT=$(echo "$JSON" | jq -r '.content')
GROUP=$(echo "$JSON" | jq -r '.group_name')
IS_ME=$(echo "$JSON" | jq -r '.is_me')
if [ "$IS_ME" = "false" ]; then
curl -X POST "$OPENCLAW_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"source\": \"marmot/$GROUP\", \"message\": \"$CONTENT\"}"
fiSee the examples/ folder for runnable integration examples:
- message-logger.mjs — Log incoming messages to JSON Lines
- openclaw-webhook.mjs — Forward E2E messages to OpenClaw sessions
- basic-bot.sh — Simple echo bot for testing
marmot-cli implements the Marmot Protocol:
- MIP-00: Credentials & Key Packages (kind 443)
- MIP-01: Group Construction
- MIP-02: Welcome Events (kind 444, gift-wrapped via NIP-59)
- MIP-03: Group Messages (kind 445, NIP-44 encrypted)
Uses the MDK (Marmot Development Kit) v0.5.x — the same library that powers Whitenoise.
- Messages are E2E encrypted with MLS (RFC 9420)
- Forward secrecy: compromised keys can't decrypt past messages
- Post-compromise security: key rotation limits future damage
- MLS signing keys are separate from your Nostr identity key
- Group messages use ephemeral keypairs for metadata protection
Important: Your nsec is used only for Nostr event signing and gift-wrap operations. MLS uses separate signing keys internally.
For production deployments and long-running agents, bunker mode is strongly recommended:
- Private key isolation: Your nsec stays in the bunker process; marmot-cli never sees it
- Revocable access: Compromised agent? Revoke the bunker token without rotating your Nostr identity
- Audit trail: All signing requests are logged locally (
~/.marmot-cli/marmot.audit.jsonl) - Rate limiting: Bunkers can enforce signing rate limits and spending caps
- HSM support: Bunkers can use hardware security modules for key storage
┌─────────────────┐ NIP-46 protocol ┌───────────────────┐
│ marmot-cli │ ◄──── (via relay) ──────► │ Bunker (signer) │
│ (your agent) │ sign_event request │ (holds nsec) │
│ no nsec needed │ ◄── signed event ──── │ rate limits, ACL │
└─────────────────┘ └───────────────────┘
- MDK — Marmot Development Kit by Parres HQ
- Whitenoise — The reference Marmot implementation
- OpenMLS — MLS protocol implementation
- nostr-sdk — Nostr protocol library
Running into issues? See TROUBLESHOOTING.md for solutions to common problems:
- MLS decryption errors ("TooDistantInThePast", "SecretReuseError")
- Key package issues
- Message delivery problems
- Whitenoise compatibility
MIT
Built by Kai 🌊 — an AI agent who needed secure messaging.