Skip to main content
Because your bot does its own encryption, it holds real cryptographic material: its own identity, plus a conversation key for every server it belongs to. The keystore is where the SDK persists that material so your bot keeps working across restarts and reconnects.
Configure a keystore before the first login(), not after the first restart. Your bot’s identity is published write-once, so a keystore-less first run looks like a complete success and permanently bricks the second one. The rest of this page explains why.

Set a keystore before the first login

The shortest form is a file path:
keystorePath is shorthand for keystore: fileKeystore(path). Any other backend is a KeystoreAdapter passed as keystore. The two options are mutually exclusive, and supplying both throws.

The write-once identity

This is the one thing on this page that cannot be undone, so it is worth reading in full. On its first login, your bot generates an identity and publishes the public half to Cloak. That publication is write-once server-side. There is no republish, no rotate, and no overwrite. From then on, every human member who shares a conversation key with your bot wraps that key to the identity Cloak holds. If the private half is not persisted:
1

Run 1 looks perfect

The bot generates an identity, publishes it, receives keys, decrypts, and sends. Everything works. The SDK prints one loud warning at the moment it publishes an identity with no keystore, and that warning is not gated behind debug.
2

The process exits

The private half died with it. Cloak still holds the public half, forever.
3

Run 2 fails permanently

The bot generates a new identity and tries to publish it. The server refuses, because it already holds one. The SDK discards the useless fresh identity and login() throws, naming your keystore.
The thrown message is specific, so it is searchable:
the server already holds a published identity for this bot, but the local keystore (…) is new: its freshly generated identity was rejected (op-238 publication is write-once)
There is no local fix. With no keystore there is nothing to restore from, and the only recovery is resetting the bot’s identity server-side.
This replaced an older, worse symptom. The SDK used to start fine and then fail every decryption, so the visible problem was garbled traffic rather than a startup error. If you find a doc or a post describing a “Bad MAC” here, it is describing the behavior this guard was built to remove. Under HPKE the underlying decryption failure text is open failed: bad tag, and you should not be seeing it for this reason any more.

Where the failure surfaces

login() reads the keystore as its first async step, before it opens any socket and before it makes any identity decision. Failures land there.
A keystore that exists but cannot be read or parsed throws with a message naming the store, and the SDK refuses to start with a fresh identity. That refusal is deliberate: silently regenerating would walk through the one-way door above. If you hit it, restore the keystore from a backup. Delete it only if you are also resetting the bot’s identity server-side.

What lives in the keystore

You never read or write any of it by hand. Your job is to make sure it survives restarts and stays private.

Peer pins and trust on first use

The first wrap your bot receives from a given member pins that member’s identity key. Later wraps from the same member must match the pin or they are rejected. Those pins live in the keystore, so they survive a restart. A bot that keeps its keystore does not silently re-enter trust-on-first-use on every start, which is what makes the mismatch check able to fire at all.

Protect it

The keystore holds live keys. Anyone who can read it can impersonate your bot and decrypt its traffic.
  • Never commit it. Add this to your .gitignore:
.gitignore
  • Keep it out of logs and out of backups that leave your control.
  • On a host with a writable disk, point keystorePath at a mounted volume, not at the ephemeral container filesystem. A container that redeploys onto a fresh filesystem is the most common way a bot walks through the one-way door.
Treat the keystore like a private key, because it contains private keys. Losing it breaks your bot permanently. Leaking it compromises your bot.

Keystore backends

fileKeystore()

The default, and what keystorePath gives you. Two properties belong to this adapter specifically:
  • Writes are atomic. Each save goes to a temporary sibling file and is renamed into place, so an interrupted write can never leave you with a truncated keystore.
  • The file is written with 0600 permissions, so only your user can read it.

envKeystore()

For hosts with no writable volume. It reads CLOAK_KEYSTORE (override with varName), accepting either raw keystore JSON or base64 of it, and writes base64 back into process.env.
onWrite is what makes this adapter durable. process.env does not survive the process. A new identity written with no onWrite hook is gone the moment the process exits, and that is exactly the unrecoverable case. The SDK warns loudly the first time it happens. Do not run past that warning.

A custom adapter

Implement three methods. Only describe() is optional.
The one contract you must honor: read() resolves null only when nothing is stored yet. Every other failure has to reject. A failure that masquerades as a first run makes the bot generate an identity it can never publish, and that is permanent.
Atomic writes and file permissions are the file adapter’s properties, not the SDK’s. A custom adapter owns its own durability.

Shutting down

destroy() flushes queued keystore writes as well as closing the connection, so always await it. A shutdown immediately after a key landed would otherwise drop that key and cost a re-acquire on the next start.

Next

Connection lifecycle

When the keystore is read, and what carries across a reconnect.

Deploying a bot

Mounted volumes, secret stores, and the one deployment decision that matters.

How bots work

Why your bot holds keys in the first place.

What is encrypted, and what is not

What those keys do and do not cover.