Why encryption matters here
Morpheus is not a chat app. The commands you send from your phone are forwarded to an AI agent that can read files, write code, run shell commands, and interact with databases on your machine. If an attacker intercepts or tampers with those messages, the consequences are severe — arbitrary code execution on your workstation.
That is why every message between the mobile app and the Morpheus Agent companion is end-to-end encrypted. Not TLS-only. Not "encrypted in transit." Full NaCl secretbox encryption where only the two paired devices hold the key.
ECDH key exchange with TweetNaCl
We use TweetNaCl's nacl.box API, which implements Curve25519-XSalsa20-Poly1305. The key exchange happens during the initial QR pairing flow:
// Agent generates a key pair on startup
const serverKeyPair = nacl.box.keyPair();
// QR code encodes the WebSocket URL + server public key
const qrPayload = JSON.stringify({
url: "ws://192.168.1.42:3847",
publicKey: encodeBase64(serverKeyPair.publicKey),
});
// Mobile scans QR, generates its own key pair
const clientKeyPair = nacl.box.keyPair();
// Mobile sends its public key to the agent
ws.send(JSON.stringify({
type: "pair_request",
publicKey: encodeBase64(clientKeyPair.publicKey),
}));
// Both sides derive the same shared secret
const sharedSecret = nacl.box.before(
otherSidePublicKey,
ownSecretKey
);The shared secret is a 32-byte key derived from the Curve25519 Diffie-Hellman exchange. Both sides compute the same value independently — it never travels over the wire.
What is in the QR code
The QR code displayed by the agent contains a JSON payload with:
- WebSocket URL — The LAN address (or Cloudflare tunnel URL for remote pairing) where the agent is listening.
- Server public key — Base64-encoded Curve25519 public key so the mobile app can immediately begin the ECDH handshake.
No secrets are embedded in the QR code. The public key is, by definition, safe to expose. The critical secret (the shared key) is derived after the handshake completes.
Challenge-response on reconnect
Once paired, the mobile app stores the shared secret in the device's secure enclave (iOS Keychain / Android Keystore via Expo SecureStore). When it reconnects — after a network change, app restart, or sleep cycle — it needs to prove it still holds the secret without retransmitting it.
// Agent sends a challenge on reconnect
const challenge = nacl.randomBytes(32);
const expected = encodeBase64(nacl.hash(challenge));
ws.send(JSON.stringify({
reconnect: true,
challenge: Array.from(challenge),
}));
// Mobile solves the challenge
const solution = encodeBase64(
nacl.hash(new Uint8Array(challenge))
);
ws.send(JSON.stringify({
type: "challenge_response",
solution,
}));
// Agent verifies: solution === expectedThe challenge is 32 random bytes. The expected answer is the Base64-encoded SHA-512 hash (via nacl.hash). Because the mobile app must produce the correct hash, an attacker cannot replay a previous session or impersonate the device.
NaCl secretbox for all messages
After pairing (or successful reconnect), every message is encrypted with nacl.secretbox — XSalsa20-Poly1305 symmetric encryption using the shared secret:
// Encrypt
const nonce = nacl.randomBytes(24);
const ciphertext = nacl.secretbox(
decodeUTF8(JSON.stringify(message)),
nonce,
sharedSecret
);
// Send as { nonce, ciphertext } (both Base64)
// Decrypt
const plaintext = nacl.secretbox.open(
ciphertext,
nonce,
sharedSecret
);Each message gets a fresh 24-byte random nonce. If either side receives a message that fails to decrypt (wrong key, tampered ciphertext), it is silently dropped. No plaintext is accepted after the shared secret is established.
No plaintext after pairing
This is a hard rule in Morpheus. Once sharedSecret is set on both sides, any incoming message that is not valid encrypted JSON is rejected. There is no fallback to plaintext, no "debug mode," and no way to downgrade the connection. If encryption fails, the connection is dropped and the user must re-pair.
Instruction sanitization
Encryption protects the transport layer, but we also need to protect against prompt injection at the application layer. Before any instruction is forwarded to Claude Code, Morpheus sanitizes the input to strip known injection patterns — things like system prompt overrides, role-switching tokens, and encoded escape sequences.
This is defense in depth. Even if an attacker somehow compromised the encrypted channel, the Morpheus Agent would still reject malformed instructions before they reach the AI model.