Kraki handles prompts, code, shell commands, and agent output. The goal is to let you use a relay without giving that relay any access to your content.
This document explains what Kraki protects, what it does not protect, and what the relay can still see.
- The relay cannot read message contents. All message bodies are end-to-end encrypted.
- The relay sees only what it needs to forward traffic: envelope type, destination device ID, sender device ID, and blob size.
- Endpoints still see plaintext: the machine running the agent and the device reading the session.
| Threat | How Kraki helps |
|---|---|
| Relay operator reading content | E2E encryption keeps message bodies on the endpoints |
| Network interception | Clients use TLS / WSS |
| Message tampering | Authenticated encryption (AES-256-GCM) detects modification |
| Unauthorized device access | Pairing, authentication, and device registration control who can join |
| Unauthorized web access | GitHub OAuth code exchange keeps client_secret server-side; CSRF state parameter prevents forged callbacks |
The relay must route traffic, so it sees some metadata. Here is the complete list:
| Visible to the relay | Why |
|---|---|
| Envelope type (unicast or broadcast) | Needed to decide fan-out behavior |
to device ID (unicast only) |
Needed to route to the right connection |
| Sender device ID (from connection) | Needed to identify origin |
| Blob size | The relay forwards blobs, so size is visible |
That is all. The following are not visible to the relay:
- Message content or message type
- Session IDs
- Tool names and agent output
- User input and approval decisions
- Sequence numbers (assigned by tentacle, inside the blob)
Kraki protects content fully, not just partially. The relay is an encrypted forwarder with no ability to inspect payloads.
The relay maintains two database tables:
- users — user identity and auth records
- devices — registered devices and their public keys
- push_tokens — push notification tokens for offline delivery (device token and provider type)
The relay does not store messages, sessions, message history, or any content. Message buffering and replay are handled by tentacle.
Kraki cannot remove trust from the endpoints themselves.
- If the machine running the agent is compromised, the attacker can see plaintext before it is encrypted.
- If the receiving device is compromised, the attacker can see plaintext after it is decrypted.
- E2E encryption does not hide traffic patterns, activity times, or the metadata listed above.
- A malicious or buggy agent can still misuse the permissions you give it.
Kraki uses:
- AES-256-GCM for message content
- RSA-OAEP (4096-bit) for per-device key wrapping
At a high level, the flow is:
- A sender creates a fresh symmetric key for one message.
- The message body is encrypted with AES-256-GCM.
- The IV, ciphertext, and authentication tag are concatenated into a single blob:
base64(iv ‖ ciphertext ‖ tag). - The symmetric key is wrapped separately for each recipient device's public key.
- The relay forwards the blob and wrapped keys without being able to read them.
- Each recipient unwraps the symmetric key with its private key and decrypts locally.
This keeps the large payload encryption fast while still allowing multiple receiving devices.
Kraki keeps private keys on the devices that use them.
- Keys are generated with the Web Crypto API
- The private key is non-extractable
- Key material is stored locally in browser-managed storage
This makes raw key export harder, but it does not make the browser magically invulnerable. A compromised browser context can still act as that device while it is running.
- Keys are generated locally on first use
- The private key is stored under
~/.kraki/keys/private.pem - File permissions are restricted to the local user
This is local-machine trust, not hardware-backed security.
A newly added device cannot automatically decrypt old messages that were encrypted for earlier devices.
To recover that history, an already-authorized online device can re-encrypt stored messages for the new device. If no existing device is online yet, the new device can still receive live traffic immediately and older history can sync later.
That behavior is a normal consequence of per-device encryption.
| Question | Answer |
|---|---|
| Can the relay read message bodies? | No |
| Can the relay see routing metadata? | Yes — envelope type, device IDs, blob size |
| Can the relay see session IDs, message types, or content? | No — all inside encrypted blob |
| Do endpoints see plaintext? | Yes |
| Does the relay store messages? | No — only user and device tables |
| Is self-hosting still useful? | Yes, for operational control and latency |
Push notifications use the same E2E encryption model. When an agent event requires attention and the browser is offline:
- The tentacle encrypts a small preview (
pushPreview) with the offline device's public key — the same RSA-OAEP wrapping used for WebSocket messages. - The relay forwards the opaque encrypted preview through the push service (APNs or Web Push/VAPID).
- The device's service worker decrypts the preview locally and shows the notification content.
The relay sees the encrypted blob size and the push token — never the notification content. This extends the same trust boundary from WebSocket delivery to push delivery.
Images produced by an agent (via the kraki-show_image MCP tool) are content-addressed and stored on the tentacle's disk under the session's directory. The bytes are not embedded in agent activity messages — only an attachment reference (id + metadata) appears in the broadcast and in messages.jsonl.
Bytes are delivered separately, encrypted per-recipient like any other message:
- On live activity, the tentacle pushes the bytes as a chunked
attachment_datastream to all session-member devices immediately after the referencing tool message. - On replay or cache miss, a receiver explicitly requests the bytes via a
request_attachmentunicast; the tentacle serves chunks back to that specific authenticated device.
The relay sees the same opaque encrypted blobs it sees for any other message, plus chunked transfer adds nothing to its visibility. Bytes never leave the tentacle except on an authenticated session-member request, so the privacy boundary for screenshots matches the privacy boundary for prompts and tool output.
The tentacle runs a small loopback HTTP server (the Kraki MCP server) so the local agent can call Kraki-specific tools such as kraki-show_image. It binds to 127.0.0.1 only, requires a per-session bearer token, and is registered with the agent via a per-session URL.
The MCP server is not exposed to the relay or to other devices. It is reachable only from processes on the same machine as the tentacle — the same trust zone as the agent itself.
Open source helps because it lets people inspect how Kraki handles encryption, routing, and storage. It does not replace operational trust by itself, but it does make the design auditable and self-hosting possible.
For the runtime picture of how the pieces fit together, read ARCHITECTURE.md.