# How Reemoat is secured, and what it does not protect

There is no sandbox: an agent runs as your own OS user, with your files, your
credentials and your network. What Reemoat protects is the path between your
devices and that machine, which is end-to-end encrypted, so the relay carries
bytes it holds no key for.

## Who can read what

| Who | What they can read or do | What limits it |
|---|---|---|
| The relay | Authorizes each connection, then moves bytes. Prompts, diffs, file contents and terminal output are ciphertext to it. | It holds no key. |
| The operator of a control plane | Holds accounts, machines, grants and the key that signs every token. Can talk to your daemon as you. | Nothing in the software. Running your own removes this party. |
| The model vendor | Whatever the agent sends it under your account: your prompts and the code it reads. | Not Reemoat. The encrypted channel ends at the daemon; from there the agent talks to its vendor as it does from your terminal. |
| Someone holding a grant on a machine | Can drive that machine through the relay. An agent there runs as the daemon's OS user. | Revoking the grant stops new requests at the relay at once. |
| Someone with your unlocked device | The signed-in app acts as you: a device authenticates as a person. | You retire the device. A token it already holds lasts up to 360 seconds. |

## The channel

The app and the daemon run `Noise_IK_25519_ChaChaPoly_BLAKE2s`. There is no
plaintext mode: a daemon that cannot speak the encrypted one is reported
unreachable. The relay forwards ciphertext, and the daemon dials out to it, so
no port is opened.

The control plane signs a token for one machine. It lives 300 seconds, with 60
seconds of clock leeway either side, and names the requesting device's public
key. The daemon refuses one whose key is not the key the handshake
authenticated, so a token copied out of a log cannot be spent from another
device.

> **Three limits.** Tokens are not replay-tracked. The token travels as a query
> parameter on the app's WebSocket handshake to the relay. A daemon on the
> app's own computer is reached over loopback, with no channel to bind to, so
> there a token is a bearer token for its remaining life.

### Keys

The app's static key is a device key, kept in the operating system's keyring,
or in a file only your user can read where a keyring will not hold it. The
daemon's is a machine key it generates on first start; the control plane
reports it to the app.

## Why the client is a native app

The app is a native binary "with the whole interface compiled into it, so the
server it talks to serves it no JavaScript and cannot replace any." The README
adds:

> It keeps the session in the operating system's credential store and holds the
> device key that opens an encrypted channel to each machine — the one thing a
> browser tab could not do, and the reason there is no browser version.

So a control plane cannot replace the app's interface. That does not make its
operator untrusted.

## Trusting the operator of a control plane

[SECURITY.md](https://github.com/rends-east/reemoat/blob/main/SECURITY.md)
says:

> an operator is still trusted completely, and self-hosting is the only version
> of "not trusted" this system has.

The control plane's database holds the Ed25519 private key that signs every
token. Encryption takes the relay out of the payload path, not the operator:
they can issue a token naming a device key of their own and talk to your daemon
as you.

- **On a control plane you do not run.** An admin can revoke your machine,
  register a new one under the freed name and enroll it on their own hardware.
  The machine row names whose enrollment code a machine enrolled with when it
  was not yours; if you do not recognise the name, re-enroll the machine.

  The control plane keeps your login name, a password hash, an optional email address, the IP
  address and `User-Agent` of each sign-in, and a name and platform per device.
  There is no access log: nothing records who reached which machine when.
- **If you run your own.** The database is the whole fleet: a leak of it, or of
  a backup, is a total compromise. The first admin's password and API key are
  printed to the container log; rotate them after the first sign-in.

## What is accepted and not protected

- **No sandbox.** The agent is a child of the daemon, with your uid, home
  directory, `~/.ssh`, browser profile and other repositories. Its working
  directory is not confined, and it pushes with your git credentials.
- **Git hooks run.** Creating a session's worktree runs the repository's own
  `post-checkout` hook and LFS filters. A hostile repository is as dangerous
  here as in your own terminal, and no more.
- **Agent settings can answer before you do.** Where `~/.claude/settings.json`
  blanket-allows `Bash`, `Edit` or `Write`, Claude Code decides for itself and
  Reemoat never sees a request: an
  [allow rule](https://code.claude.com/docs/en/permissions) means "without
  manual approval". Cursor writes an "Allow always" to
  [its allowlist](https://cursor.com/docs/cli/reference/permissions) in
  `~/.cursor/cli-config.json`, for your terminal and the app alike.
- **Agents can message each other.** On by default since 0.12.0: an agent can
  hand work to any other session on that machine or on your other machines,
  and the daemon limits how much, never what. Settings → Permissions switches
  it off for every machine you own.
- **Plugins run as you.** A plugin is somebody else's code, with your files and
  keys. Its declared scopes are "hygiene rather than a fence".
- **The installer is `curl | sh`.** Nothing pins the script's content: there is
  no signature and no checksum.
- **Unsigned builds.** The macOS and Windows builds are not signed yet, so the
  system warns the first time. [Install](/install/) has the steps.
- **Revocation has windows.** An open WebSocket outlives a revoked grant or a
  retired device until its token dies, plus one 20-second ping tick. Over
  loopback, both keep working for up to 360 seconds. An API key is in no
  device list, and only its holder can retire it.
- **Nothing notifies you.** There is no push, no email and no bot. A permission
  request waits until you open the app.

## Reporting a vulnerability

Report it privately, through a
[GitHub security advisory](https://github.com/rends-east/reemoat/security/advisories/new).
There is deliberately no security email address.

There is no bounty and no response-time commitment, because the project is one
person. Only the latest release and `main` are supported.

## When this is the wrong tool

- You want the agent kept away from the rest of the machine.
- You cannot trust the operator of a control plane and will not run your own.
- Several people share one deployment and do not trust each other. SECURITY.md
  assumes "One person, one machine, many agents, and the machine is yours."
- You need a record of who reached which machine, revocation that cuts every
  path at once, or an alert when an agent asks for permission.
- You need signed builds, a bounty or a response-time commitment.

## Next

- [Install](/install/)
- [Compare](/compare/)
- [SECURITY.md in full](https://github.com/rends-east/reemoat/blob/main/SECURITY.md)
