Broken today
fetchMembers() hangs
A backend change made the underlying opcode direct-message-only, so a server-scoped request produces no reply frame of any kind, not even an error code. This is not flakiness and it is not a permission problem. It affects every bot on every server.
The practical consequence is worth stating plainly: try/catch does not save you, because nothing is thrown, and await blocks the code path it is on. If you have a command that enumerates members, it will appear to do nothing forever.
fetchRoles() is unaffected and works normally. So does everything else in the read surface.
The rebuild is planned and the signature will survive it, so code written against fetchMembers() stays valid. It just cannot run today.
Not exposed to bots
Direct message reads, edits, reactions and pins
A bot can hold a 1:1 end-to-end encrypted DM with a human, and both directions work. What does not work inside a DM:
The backend implements most of these operations for DMs, but their result hooks never reach a bot, so the SDK would be issuing writes it can never observe.
reply() and channel.send() work normally, and messageCreate still fires, so you can parse msg.content yourself if you want DM commands.
Group DMs
Not implemented. The group-DM lane has a different, versioned key model. 1:1 DMs are the whole DM surface today.The REST lane is off by default
client.rest is disabled server-side. The first call rejects with a message saying the flag is unset. When an operator does enable it, the credential is scoped behind a default-deny allowlist that denies the custom-emoji and audit-log reads, which is why the SDK ships no fetchCustomEmojis() and no fetchAuditLog(). See What the server must support.
The typing event has no channel
serverId and channelId on the typing event are always null. This is a wire fact, not a decode gap: the relay frame carries no routing tail, and the source channel travels in the server’s internal fanout envelope, which never reaches the client.
The fields are typed nullable so that a future backend appending routing is an additive change rather than a breaking one.
Voice
Bots cannot publish video, and will not
A bot never gets a subscription tier, and the backend restricts a bot’s publish sources to microphone at token-mint time. Camera and screenshare are denied at the credential, not by the SDK, so there is no path to enable and nothing to configure.p2pWebcam and p2pStream are dead
They remain on the type for compatibility with code written before the removal.
A bot cannot detect a voice encryption failure
The voice media engine surfaces no per-participant end-to-end encryption failure state. The generic encryption-error event carries no identity, missing-key and decryption-failed conditions are dropped, and a keyless frame decryptor retries quietly forever. The one per-participant event that exists reports that frames are marked encrypted and stays true through the exact key and identity mismatch cases you would want it to catch. So a bot cannot detect that nobody can decrypt it, and a listening bot cannot detect that it is failing to decrypt someone. Silence is the only symptom, and nothing reports it. Because of that, the SDK ships no voice health check and no retry-on-decrypt-failure path. Both would be theatre. The defence is to be right by construction:attach() installs exactly the key material the key manager returns, under the identity the engine reported, before publishing, and fails closed on every precondition. If members hear silence, suspect the key or the identity rather than the network.
Related: a bot without voice_listen holds only its own frame key and is cryptographically unable to decrypt any member. A bot granted voice_listen can decrypt everyone in the channel, by design. Never quote the first sentence without the second. Also, a voice_listen bot must join non-deafened, because subscription is baked into the join credential.
Encryption boundaries
Message bodies are end-to-end encrypted. Two deliberate exceptions are worth knowing before you design around “the server cannot read anything”.Mention targets ride in plaintext
A Cloak mention is two independent things: pill text inside the encrypted body, and a plaintext[id, isRole] pair in the send frame. The server has to read the second one to fan out notifications without decrypting the first.
The webhook embed lane is not encrypted at all
sendEmbed() and postToWebhook() post over a lane that is stored as plaintext server-side and renders under a webhook identity, not your bot’s. The payload type is named UnencryptedWebhookPost on purpose, and unencrypted: true is required and enforced at runtime.
Use sendCard() when you want a rich card that stays encrypted and posts as the bot. See Choosing an embed lane.
Rich cards
Media must be Cloak-hosted
The four slots a card fetches with no user action,image.url, thumbnail.url, author.icon_url and footer.icon_url, must have origin exactly https://media.cloak.chat. Anything else throws a CloakEmbedError locally, before a frame leaves the process.
Rendering a card fetches its media, which reveals the viewer’s IP address to whoever hosts the URL. On this lane that URL is chosen by a third-party bot author who could also serve different bytes to different viewers, with no new message for moderation to see. The server cannot rewrite anything here, because it cannot read the payload.
There is deliberately no option, environment variable or flag that widens the allowed origin. The link slots (url, author.url, provider.url) are unrestricted, because nothing is fetched for them at render.
Where a Cloak-hosted URL comes from today: an asset a human uploaded through the Cloak app, your bot’s own avatar, or a room icon. A bot-side upload helper is designed but not shipped, because it needs the REST lane that is disabled server-side.
fields render on desktop only
Three keys that exist on the webhook lane and do not exist on the card lane: video (removed entirely, passing it throws), provider.icon, and unknown keys (rejected rather than silently dropped, because the renderer would drop them while they still counted against the payload budget).
Slash commands, v1
Two properties you have to design around rather than work around.There is no ephemerality
There is no ephemerality
An invocation is a normal message encrypted with the channel’s key. Every member holding that key can read every invocation, and it is a durable message row. There is no “only you can see this”. Ephemeral responses need a different key lane and are a later version. Do not fake them by deleting the message afterwards.
login(), because that is when the menu is published and the SDK does not re-publish on its own, and commands are never dispatched from DMs.
Things that are easy to misread as bugs
Next
When a bot will not start
Symptom-indexed triage for the failures you can see.
What the server must support
The server-side half of this list.
Deploying a bot
Getting a bot onto a host without tripping over the keystore.
Changelog and migration
What changed in 0.2.0, including the breaking changes.